diff --git a/SciDataTool/Classes/Class_Dict.json b/SciDataTool/Classes/Class_Dict.json index 6d7abc19..efd51f8e 100644 --- a/SciDataTool/Classes/Class_Dict.json +++ b/SciDataTool/Classes/Class_Dict.json @@ -79,7 +79,9 @@ "get_axis_periodic", "has_period", "get_periodicity", - "to_linspace" + "to_linspace", + "get_filter", + "check_filter" ], "mother": "Data", "name": "Data1D", @@ -121,6 +123,33 @@ "type": "bool", "unit": "", "value": false + }, + { + "desc": "Character used to separate attributes in string case (e.g. \"r=0, stator, radial\")", + "max": "", + "min": "", + "name": "delimiter", + "type": "str", + "unit": "", + "value": "None" + }, + { + "desc": "List of indices to use to sort axis", + "max": "", + "min": "", + "name": "sort_indices", + "type": "list", + "unit": "", + "value": null + }, + { + "desc": "Dict of filter keys", + "max": "", + "min": "", + "name": "filter", + "type": "dict", + "unit": "", + "value": null } ] }, diff --git a/SciDataTool/Classes/Data1D.py b/SciDataTool/Classes/Data1D.py index 7317e76d..85ea313f 100644 --- a/SciDataTool/Classes/Data1D.py +++ b/SciDataTool/Classes/Data1D.py @@ -45,6 +45,16 @@ except ImportError as error: to_linspace = error +try: + from ..Methods.Data1D.get_filter import get_filter +except ImportError as error: + get_filter = error + +try: + from ..Methods.Data1D.check_filter import check_filter +except ImportError as error: + check_filter = error + from numpy import array, array_equal from numpy import isnan @@ -117,6 +127,26 @@ class Data1D(Data): ) else: to_linspace = to_linspace + # cf Methods.Data1D.get_filter + if isinstance(get_filter, ImportError): + get_filter = property( + fget=lambda x: raise_( + ImportError("Can't use Data1D method get_filter: " + str(get_filter)) + ) + ) + else: + get_filter = get_filter + # cf Methods.Data1D.check_filter + if isinstance(check_filter, ImportError): + check_filter = property( + fget=lambda x: raise_( + ImportError( + "Can't use Data1D method check_filter: " + str(check_filter) + ) + ) + ) + else: + check_filter = check_filter # save and copy methods are available in all object save = save copy = copy @@ -127,6 +157,9 @@ def __init__( is_components=False, symmetries=-1, is_overlay=False, + delimiter=None, + sort_indices=None, + filter=None, symbol="", name="", unit="", @@ -157,6 +190,12 @@ def __init__( symmetries = init_dict["symmetries"] if "is_overlay" in list(init_dict.keys()): is_overlay = init_dict["is_overlay"] + if "delimiter" in list(init_dict.keys()): + delimiter = init_dict["delimiter"] + if "sort_indices" in list(init_dict.keys()): + sort_indices = init_dict["sort_indices"] + if "filter" in list(init_dict.keys()): + filter = init_dict["filter"] if "symbol" in list(init_dict.keys()): symbol = init_dict["symbol"] if "name" in list(init_dict.keys()): @@ -170,6 +209,9 @@ def __init__( self.is_components = is_components self.symmetries = symmetries self.is_overlay = is_overlay + self.delimiter = delimiter + self.sort_indices = sort_indices + self.filter = filter # Call Data init super(Data1D, self).__init__( symbol=symbol, name=name, unit=unit, normalizations=normalizations @@ -193,6 +235,14 @@ def __str__(self): Data1D_str += "is_components = " + str(self.is_components) + linesep Data1D_str += "symmetries = " + str(self.symmetries) + linesep Data1D_str += "is_overlay = " + str(self.is_overlay) + linesep + Data1D_str += 'delimiter = "' + str(self.delimiter) + '"' + linesep + Data1D_str += ( + "sort_indices = " + + linesep + + str(self.sort_indices).replace(linesep, linesep + "\t") + + linesep + ) + Data1D_str += "filter = " + str(self.filter) + linesep return Data1D_str def __eq__(self, other): @@ -212,6 +262,12 @@ def __eq__(self, other): return False if other.is_overlay != self.is_overlay: return False + if other.delimiter != self.delimiter: + return False + if other.sort_indices != self.sort_indices: + return False + if other.filter != self.filter: + return False return True def compare(self, other, name="self", ignore_list=None): @@ -233,6 +289,12 @@ def compare(self, other, name="self", ignore_list=None): diff_list.append(name + ".symmetries") if other._is_overlay != self._is_overlay: diff_list.append(name + ".is_overlay") + if other._delimiter != self._delimiter: + diff_list.append(name + ".delimiter") + if other._sort_indices != self._sort_indices: + diff_list.append(name + ".sort_indices") + if other._filter != self._filter: + diff_list.append(name + ".filter") # Filter ignore differences diff_list = list(filter(lambda x: x not in ignore_list, diff_list)) return diff_list @@ -250,6 +312,13 @@ def __sizeof__(self): for key, value in self.symmetries.items(): S += getsizeof(value) + getsizeof(key) S += getsizeof(self.is_overlay) + S += getsizeof(self.delimiter) + if self.sort_indices is not None: + for value in self.sort_indices: + S += getsizeof(value) + if self.filter is not None: + for key, value in self.filter.items(): + S += getsizeof(value) + getsizeof(key) return S def as_dict(self, type_handle_ndarray=0, keep_function=False, **kwargs): @@ -287,6 +356,11 @@ def as_dict(self, type_handle_ndarray=0, keep_function=False, **kwargs): self.symmetries.copy() if self.symmetries is not None else None ) Data1D_dict["is_overlay"] = self.is_overlay + Data1D_dict["delimiter"] = self.delimiter + Data1D_dict["sort_indices"] = ( + self.sort_indices.copy() if self.sort_indices is not None else None + ) + Data1D_dict["filter"] = self.filter.copy() if self.filter is not None else None # The class name is added to the dict for deserialisation purpose # Overwrite the mother class name Data1D_dict["__class__"] = "Data1D" @@ -299,6 +373,9 @@ def _set_None(self): self.is_components = None self.symmetries = None self.is_overlay = None + self.delimiter = None + self.sort_indices = None + self.filter = None # Set to None the properties inherited from Data super(Data1D, self)._set_None() @@ -382,3 +459,61 @@ def _set_is_overlay(self, value): :Type: bool """, ) + + def _get_delimiter(self): + """getter of delimiter""" + return self._delimiter + + def _set_delimiter(self, value): + """setter of delimiter""" + check_var("delimiter", value, "str") + self._delimiter = value + + delimiter = property( + fget=_get_delimiter, + fset=_set_delimiter, + doc=u"""Character used to separate attributes in string case (e.g. "r=0, stator, radial") + + :Type: str + """, + ) + + def _get_sort_indices(self): + """getter of sort_indices""" + return self._sort_indices + + def _set_sort_indices(self, value): + """setter of sort_indices""" + if type(value) is int and value == -1: + value = list() + check_var("sort_indices", value, "list") + self._sort_indices = value + + sort_indices = property( + fget=_get_sort_indices, + fset=_set_sort_indices, + doc=u"""List of indices to use to sort axis + + :Type: list + """, + ) + + def _get_filter(self): + """getter of filter""" + return self._filter + + def _set_filter(self, value): + """setter of filter""" + if type(value) is int and value == -1: + value = dict() + check_var("filter", value, "dict") + self._filter = value + + filter = property( + fget=_get_filter, + fset=_set_filter, + doc=u"""Dict of filter keys + + :Type: dict + """, + ) diff --git a/SciDataTool/GUI/DDataPlotter/DDataPlotter.py b/SciDataTool/GUI/DDataPlotter/DDataPlotter.py index f23282b2..237163ee 100644 --- a/SciDataTool/GUI/DDataPlotter/DDataPlotter.py +++ b/SciDataTool/GUI/DDataPlotter/DDataPlotter.py @@ -95,7 +95,7 @@ def __init__( plot_arg_dict : dict Dictionnary with arguments that must be given to the plot frozen_type : int - 0 to let the user modify the axis of the plot, 1 to let him switch them, 2 to not let him change them, 3 to freeze both axes and operations + 0 to let the user modify the axis of the plot, 1 to let him switch them, 2 to not let him change them, 3 to freeze both axes and operations, 4 to freeze fft """ # Build the interface according to the .ui file @@ -541,6 +541,7 @@ def set_info( axes_request_list=None, plot_arg_dict=None, is_keep_config=False, + frozen_type=0, ): """Method to set the DDataPlotter with information given self : DDataPlotter @@ -551,6 +552,8 @@ def set_info( list of RequestedAxis which are the info given for the autoplot (for the axes and DataSelection) plot_arg_dict : dict Dictionnary with arguments that must be given to the plot + frozen_type : int + 0 to let the user modify the axis of the plot, 1 to let him switch them, 2 to not let him change them, 3 to freeze both axes and operations, 4 to freeze fft """ self.plot_arg_dict = plot_arg_dict @@ -560,6 +563,7 @@ def set_info( unit=unit, axes_request_list=axes_request_list, is_keep_config=is_keep_config, + frozen_type=frozen_type, ) def showEvent(self, ev): diff --git a/SciDataTool/GUI/WAxisManager/WAxisManager.py b/SciDataTool/GUI/WAxisManager/WAxisManager.py index f5620120..d9347ff8 100644 --- a/SciDataTool/GUI/WAxisManager/WAxisManager.py +++ b/SciDataTool/GUI/WAxisManager/WAxisManager.py @@ -4,6 +4,7 @@ from SciDataTool.GUI.WAxisManager.Ui_WAxisManager import Ui_WAxisManager from SciDataTool.GUI.WSliceOperator.WSliceOperator import WSliceOperator from SciDataTool.Functions import axes_dict, rev_axes_dict +from SciDataTool.Functions.Plot import ifft_dict EXTENSION_DICT = { @@ -216,7 +217,7 @@ def set_axis_widgets( axes_request_list: list of RequestedAxis which are the info given for the autoplot (for the axes and DataSelection) frozen_type : int - 0 to let the user modify the axis of the plot, 1 to let him switch them, 2 to not let him change them, 3 to freeze both axes and operations + 0 to let the user modify the axis of the plot, 1 to let him switch them, 2 to not let him change them, 3 to freeze both axes and operations, 4 to freeze fft """ if is_keep_config: # Only update slider for wid in self.w_slice_op: @@ -243,6 +244,10 @@ def set_axis_widgets( self.w_axis_1.blockSignals(True) self.w_axis_2.blockSignals(True) + # Reinitialize filter indices + self.w_axis_1.indices = None + self.w_axis_2.indices = None + if axes_request_list == []: # Case where no user_input was given # Sending the info of data to the widget (mainly the axis) @@ -329,6 +334,14 @@ def set_axis_widgets( if len(axes_list) == 1: self.w_axis_2.hide() + elif frozen_type == 4: + self.w_axis_1.c_axis.setDisabled(True) + self.w_axis_2.c_axis.setDisabled(True) + if axes_list[0].name in ifft_dict: + self.w_axis_1.c_action.setDisabled(True) + if len(axes_list) == 2 and axes_list[1].name in ifft_dict: + self.w_axis_2.c_action.setDisabled(True) + self.w_axis_1.blockSignals(False) self.w_axis_2.blockSignals(False) diff --git a/SciDataTool/GUI/WAxisSelector/WAxisSelector.py b/SciDataTool/GUI/WAxisSelector/WAxisSelector.py index 5a50da0d..1e4f7e38 100644 --- a/SciDataTool/GUI/WAxisSelector/WAxisSelector.py +++ b/SciDataTool/GUI/WAxisSelector/WAxisSelector.py @@ -1,6 +1,7 @@ from PySide2.QtWidgets import QWidget from SciDataTool.GUI.WAxisSelector.Ui_WAxisSelector import Ui_WAxisSelector +from SciDataTool.GUI.WFilter.WFilter import WFilter from PySide2.QtCore import Signal from SciDataTool.Functions.Plot import ( unit_dict, @@ -39,14 +40,17 @@ def __init__(self, parent=None): self.name = "X" # Name of the axis self.axes_list = list() # List of the different axes of the DataND object self.norm_list = None # List of available normalizations for each axis + self.indices = None self.axis_selected = "None" # Name of the axis selected (time, angle...) self.norm = None # Name of the unit of the axis (s,m...) - self.b_filter.setDisabled(True) + self.b_filter.setEnabled(False) + self.current_dialog = None self.c_axis.currentTextChanged.connect(self.update_axis) self.c_action.currentTextChanged.connect(self.update_action) self.c_unit.currentTextChanged.connect(self.update_unit) + self.b_filter.clicked.connect(self.open_filter) def get_axes_name(self): """Method that return the axes that can be selected @@ -75,6 +79,13 @@ def get_axis_unit_selected(self): axis_unit_selected = self.get_axis_selected() + # Add indices if necessary + if self.get_current_action_name() == "Filter": + if self.indices is not None: + axis_unit_selected += str(self.indices) + else: + axis_unit_selected += "[]" + if self.norm is not None: # Add normalization axis_unit_selected += "->" + self.norm @@ -112,6 +123,26 @@ def get_axis_selected(self): else: return self.axis_selected + def open_filter(self): + """Open the Filter widget""" + # Close previous dialog + if self.current_dialog is not None: + self.current_dialog.close() + self.current_dialog.setParent(None) + self.current_dialog = None + + axis_selected_obj = [ + ax for ax in self.axes_list_obj if ax.name == self.axis_selected + ][0] + + self.current_dialog = WFilter(axis_selected_obj, self.indices) + self.current_dialog.refreshNeeded.connect(self.update_indices) + self.current_dialog.show() + + def update_indices(self): + self.indices = self.current_dialog.indices + self.refreshNeeded.emit() + def remove_axis(self, axis_to_remove): """Method that remove a given axis from the axis ComboBox. Parameters @@ -340,7 +371,7 @@ def update(self, axes_list, axis_name="X"): axis_name : string string that will set the text of in_name (=name of the axis) """ - + self.axes_list_obj = axes_list self.set_name(axis_name) self.set_axis_options(axes_list) self.update_axis() @@ -383,15 +414,18 @@ def update_axis(self, is_refresh=True): self.c_action.blockSignals(True) if self.axis_selected in fft_dict: - - action = ["None", "FFT", "Filter"] - self.c_action.clear() - self.c_action.addItems(action) - - else: + action = ["None", "FFT"] + elif ( + self.axis_selected in [axis.name for axis in self.axes_list_obj] + and self.axes_list_obj[ + [axis.name for axis in self.axes_list_obj].index(self.axis_selected) + ].is_components + ): action = ["None", "Filter"] - self.c_action.clear() - self.c_action.addItems(action) + else: + action = ["None"] + self.c_action.clear() + self.c_action.addItems(action) self.c_action.blockSignals(False) self.c_action.view().setMinimumWidth(max([len(ac) for ac in action]) * 6) @@ -412,10 +446,10 @@ def update_action(self): """ # If the action selected is filter, then we enable the button if self.c_action.currentText() == "Filter": - self.b_filter.setDisabled(False) + self.b_filter.setEnabled(True) else: - self.b_filter.setDisabled(True) + self.b_filter.setEnabled(False) # Converting the axes according to action selected if possible/necessary if self.c_action.currentText() == "FFT" and self.axis_selected in fft_dict: diff --git a/SciDataTool/GUI/WFilter/Ui_WFilter.py b/SciDataTool/GUI/WFilter/Ui_WFilter.py new file mode 100644 index 00000000..69c33a28 --- /dev/null +++ b/SciDataTool/GUI/WFilter/Ui_WFilter.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +# File generated according to WFilter.ui +# WARNING! All changes made in this file will be lost! +## WARNING! All changes made in this file will be lost when recompiling UI file! +################################################################################ + +from PySide2.QtCore import * +from PySide2.QtGui import * +from PySide2.QtWidgets import * + + +class Ui_WFilter(object): + def setupUi(self, WFilter): + if not WFilter.objectName(): + WFilter.setObjectName(u"WFilter") + WFilter.resize(831, 644) + WFilter.setMinimumSize(QSize(630, 470)) + WFilter.setMaximumSize(QSize(16777215, 16777215)) + self.gridLayout_4 = QGridLayout(WFilter) + self.gridLayout_4.setObjectName(u"gridLayout_4") + self.horizontalLayout = QHBoxLayout() + self.horizontalLayout.setObjectName(u"horizontalLayout") + self.horizontalSpacer = QSpacerItem( + 40, 20, QSizePolicy.Expanding, QSizePolicy.Minimum + ) + + self.horizontalLayout.addItem(self.horizontalSpacer) + + self.b_Ok = QPushButton(WFilter) + self.b_Ok.setObjectName(u"b_Ok") + + self.horizontalLayout.addWidget(self.b_Ok) + + self.b_cancel = QPushButton(WFilter) + self.b_cancel.setObjectName(u"b_cancel") + + self.horizontalLayout.addWidget(self.b_cancel) + + self.gridLayout_4.addLayout(self.horizontalLayout, 1, 0, 1, 1) + + self.tab_indices = QTableView(WFilter) + self.tab_indices.setObjectName(u"tab_indices") + self.tab_indices.setSortingEnabled(True) + self.tab_indices._rows = 0 + self.tab_indices._columns = 0 + + self.gridLayout_4.addWidget(self.tab_indices, 0, 0, 1, 1) + + self.retranslateUi(WFilter) + + QMetaObject.connectSlotsByName(WFilter) + + # setupUi + + def retranslateUi(self, WFilter): + WFilter.setWindowTitle(QCoreApplication.translate("WFilter", u"WFilter", None)) + self.b_Ok.setText(QCoreApplication.translate("WFilter", u"Ok", None)) + self.b_cancel.setText(QCoreApplication.translate("WFilter", u"Cancel", None)) + + # retranslateUi diff --git a/SciDataTool/GUI/WFilter/WFilter.py b/SciDataTool/GUI/WFilter/WFilter.py new file mode 100644 index 00000000..f673597d --- /dev/null +++ b/SciDataTool/GUI/WFilter/WFilter.py @@ -0,0 +1,289 @@ +from PySide2.QtWidgets import ( + QWidget, + QStyle, + QStyledItemDelegate, + QStyleOptionButton, + QApplication, +) +from PySide2.QtCore import Qt, QEvent, QPoint, QRect +from PySide2.QtGui import QStandardItemModel, QStandardItem +from PySide2.QtCore import Signal, QSortFilterProxyModel + +from numpy import array + +from SciDataTool.GUI.WFilter.Ui_WFilter import Ui_WFilter +from SciDataTool.Functions.Plot import axes_dict + +# Column id +VALUE_COL = 0 +PLOT_COL = 1 + + +class WFilter(Ui_WFilter, QWidget): + """Widget to select the Data/output range""" + + refreshNeeded = Signal() + + def __init__(self, axis, indices=None, parent=None): + """Linking the button with their method + initializing the arguments used + + Parameters + ---------- + self : WDataRange + a WDataRange object + parent : QWidget + The parent QWidget + """ + + # Build the interface according to the .ui file + QWidget.__init__(self, parent=parent) + self.setupUi(self) + + self.axis = axis + self.indices = indices + self.axis_values = self.axis.get_values() + self.init_table() + tableModel = self.tab_indices.model() + proxyModel = QSortFilterProxyModel() + proxyModel.setSourceModel(tableModel) + self.tab_indices.setModel(proxyModel) + + if self.axis.name in axes_dict: + self.setWindowTitle("Filtering on " + axes_dict[self.axis.name]) + else: + self.setWindowTitle("Filtering on " + self.axis.name) + + self.b_Ok.clicked.connect(self.update_and_close) + self.b_cancel.clicked.connect(self.cancel_and_close) + + def cancel_and_close(self): + """Method called when the user click on the cancel button + Parameters + ---------- + self : WDataRange + a WDataRange object + """ + + self.close() + + def init_table(self): + """Method that fill the table with the values of the axis, each line corresponds to one index + Parameters + ---------- + self : WDataRange + a WDataRange object""" + self.tab_indices.setSortingEnabled(True) + + if self.axis.is_components: + # If we have an axis with components, then we use the filters to build the complete table + if hasattr(self.axis, "filter") and self.axis.filter is not None: + filter_list = list(self.axis.filter.keys()) + else: + ncol = len(self.axis_values[0].split(self.axis.delimiter)) + filter_list = ["" for i in range(ncol)] + filter_list.append("Plot ?") # Adding the column with checkbox + + # Setting up the table + data_model = MyTableModel( + [ + [string for string in value.split(self.axis.delimiter)] + for value in self.axis_values + ], + filter_list, + self, + ) + for i in range(len(self.axis_values)): + item = QStandardItem() + data_model.setItem(i, len(filter_list), item) + self.tab_indices.setModel(data_model) + + self.tab_indices.setItemDelegateForColumn( + len(filter_list) - 1, CheckBoxDelegate(self, data_model, self.indices) + ) + + else: + # Setting up the table + data_model = MyTableModel( + array([[format(value, ".4g") for value in self.axis_values]]).T, + ["Value", "Plot ?"], + self, + ) + for i in range(len(self.axis_values)): + item = QStandardItem() + data_model.setItem(i, 2, item) + self.tab_indices.setModel(data_model) + + self.tab_indices.setItemDelegateForColumn( + 1, CheckBoxDelegate(self, data_model, self.indices) + ) + + def update_and_close(self): + """Method called when the click on the Ok button + Parameters + ---------- + self : WDataRange + a WDataRange object + """ + + # Get checked indices + indices = [] + for i in range(self.tab_indices.model().rowCount()): + # Get checked indices + if ( + self.tab_indices.model() + .data( + self.tab_indices.model().index( + i, self.tab_indices.model().columnCount() - 1 + ) + ) + .checkState() + .__bool__() + ): # Use mapping to get indices before sorting + indices.append( + self.tab_indices.model() + .mapToSource(self.tab_indices.model().index(i, 0)) + .row() + ) + if indices != self.indices: + self.indices = indices + self.refreshNeeded.emit() + self.close() + + +class MyTableModel(QStandardItemModel): + def __init__(self, datain, header, parent=None): + QStandardItemModel.__init__(self, parent) + self.arraydata = datain + self.header_labels = header + + def rowCount(self, parent): + return len(self.arraydata) + + def columnCount(self, parent): + return len(self.header_labels) + + def data(self, index, role): + if role == Qt.EditRole: + return None + elif role != Qt.DisplayRole: + return None + if index.column() == self.columnCount(None) - 1: + return self.itemFromIndex(index) + else: + return self.arraydata[index.row()][index.column()] + + def headerData(self, section, orientation, role=Qt.DisplayRole): + if role == Qt.DisplayRole and orientation == Qt.Horizontal: + return self.header_labels[section] + return QStandardItemModel.headerData(self, section, orientation, role) + + def setData(self, index, value, role): + if role == Qt.EditRole: + if index.column() == self.columnCount(None) - 1: + self.itemFromIndex(index).setCheckState(Qt.CheckState(value)) + else: + super(MyTableModel, self).setData(index, value, role) + return value + + +class CheckBoxDelegate(QStyledItemDelegate): + """ + A delegate that places a fully functioning QCheckBox in every + cell of the column to which it's applied + """ + + def __init__(self, parent, model, indices=None): + QStyledItemDelegate.__init__(self, parent) + self.model = model + # Check all boxes at initialization + if indices is None: + indices = [i for i in range(self.model.rowCount(None))] + for index in indices: + self.model.setData( + self.model.index(index, self.model.columnCount(None) - 1), + True, + Qt.EditRole, + ) + + def createEditor(self, parent, option, index): + """ + Important, otherwise an editor is created if the user clicks in this cell. + ** Need to hook up a signal to the model + """ + return None + + def paint(self, painter, option, index): + """ + Paint a checkbox without the label. + """ + + checked = index.data().checkState().__bool__() + check_box_style_option = QStyleOptionButton() + + if (index.flags() & Qt.ItemIsEditable) > 0: + check_box_style_option.state |= QStyle.State_Enabled + else: + check_box_style_option.state |= QStyle.State_ReadOnly + + if checked: + check_box_style_option.state |= QStyle.State_On + else: + check_box_style_option.state |= QStyle.State_Off + + check_box_style_option.rect = self.getCheckBoxRect(option) + + check_box_style_option.state |= QStyle.State_Enabled + + QApplication.style().drawControl( + QStyle.CE_CheckBox, check_box_style_option, painter + ) + + def editorEvent(self, event, model, option, index): + """ + Change the data in the model and the state of the checkbox + if the user presses the left mousebutton or presses + Key_Space or Key_Select and this cell is editable. Otherwise do nothing. + """ + if not (index.flags() & Qt.ItemIsEditable) > 0: + return False + + # Do not change the checkbox-state + if event.type() == QEvent.MouseButtonPress: + return False + if ( + event.type() == QEvent.MouseButtonRelease + or event.type() == QEvent.MouseButtonDblClick + ): + if event.button() != Qt.LeftButton or not self.getCheckBoxRect( + option + ).contains(event.pos()): + return False + if event.type() == QEvent.MouseButtonDblClick: + return True + elif event.type() == QEvent.KeyPress: + if event.key() != Qt.Key_Space and event.key() != Qt.Key_Select: + return False + else: + return False + + # Change the checkbox-state + self.setModelData(None, model, index) + return True + + def setModelData(self, editor, model, index): + """ + The user wanted to change the old state in the opposite. + """ + newValue = not index.data().checkState() + model.setData(index, newValue, Qt.EditRole) + + def getCheckBoxRect(self, option): + check_box_style_option = QStyleOptionButton() + check_box_rect = QApplication.style().subElementRect( + QStyle.SE_CheckBoxIndicator, check_box_style_option, None + ) + check_box_point = QPoint( + option.rect.x() + option.rect.width() / 2 - check_box_rect.width() / 2, + option.rect.y() + option.rect.height() / 2 - check_box_rect.height() / 2, + ) + return QRect(check_box_point, check_box_rect.size()) \ No newline at end of file diff --git a/SciDataTool/GUI/WFilter/WFilter.ui b/SciDataTool/GUI/WFilter/WFilter.ui new file mode 100644 index 00000000..1ead873b --- /dev/null +++ b/SciDataTool/GUI/WFilter/WFilter.ui @@ -0,0 +1,77 @@ + + + WFilter + + + + 0 + 0 + 831 + 644 + + + + + 630 + 470 + + + + + 16777215 + 16777215 + + + + WFilter + + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + Ok + + + + + + + Cancel + + + + + + + + + true + + + 0 + + + 0 + + + + + + + + diff --git a/SciDataTool/GUI/WFilter/__init__.py b/SciDataTool/GUI/WFilter/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/SciDataTool/GUI/WPlotManager/WPlotManager.py b/SciDataTool/GUI/WPlotManager/WPlotManager.py index c88cd429..427bb72c 100644 --- a/SciDataTool/GUI/WPlotManager/WPlotManager.py +++ b/SciDataTool/GUI/WPlotManager/WPlotManager.py @@ -191,7 +191,7 @@ def set_info( z_max : float Minimum value for Z axis (or Y if only one axe) frozen_type : int - 0 to let the user modify the axis of the plot, 1 to let him switch them, 2 to not let him change them, 3 to freeze both axes and operations + 0 to let the user modify the axis of the plot, 1 to let him switch them, 2 to not let him change them, 3 to freeze both axes and operations, 4 to freeze fft """ # Recovering the object that we want to show self.data = data diff --git a/SciDataTool/GUI/WSliceOperator/WSliceOperator.py b/SciDataTool/GUI/WSliceOperator/WSliceOperator.py index 75cda87a..e914cc23 100644 --- a/SciDataTool/GUI/WSliceOperator/WSliceOperator.py +++ b/SciDataTool/GUI/WSliceOperator/WSliceOperator.py @@ -1,6 +1,7 @@ from PySide2.QtWidgets import QWidget from SciDataTool.GUI.WSliceOperator.Ui_WSliceOperator import Ui_WSliceOperator +from SciDataTool.GUI.WFilter.WFilter import WFilter from PySide2.QtCore import Signal from SciDataTool.Functions.Plot import axes_dict, fft_dict, ifft_dict, unit_dict from SciDataTool.Classes.Data import Data @@ -50,10 +51,13 @@ def __init__(self, parent=None): self.setupUi(self) self.name = "angle" self.axis = Data + self.current_dialog = None + self.indices = None self.c_operation.currentTextChanged.connect(self.update_layout) self.slider.valueChanged.connect(self.update_floatEdit) self.lf_value.editingFinished.connect(self.update_slider) + self.b_action.clicked.connect(self.open_filter) def get_operation_selected(self): """Method that return a string of the action selected by the user on the axis of the widget. @@ -85,21 +89,10 @@ def get_operation_selected(self): return fft_dict[self.axis_name] + action elif action_type == "overlay": - # indices = self.axis.get_values() - - # action = "[" - # for idx in range(len(indices) - 1): - # if isinstance(indices[idx], str): - # action += str(indices[idx]) + "," - # else: - # action += str(int(indices[idx])) + "," - - # if isinstance(indices[-1], str): - # action += str(indices[-1]) + "]" - # else: - # action += str(int(indices[-1])) + "]" - - return self.axis_name + "[]" + if self.indices is None: + return self.axis_name + "[]" + else: + return self.axis_name + str(self.indices) elif action_type in type_extraction_dict: action = type_extraction_dict[action_type] @@ -116,6 +109,22 @@ def get_name(self): """ return self.name + def open_filter(self): + """Open the Filter widget""" + # Close previous dialog + if self.current_dialog is not None: + self.current_dialog.close() + self.current_dialog.setParent(None) + self.current_dialog = None + + self.current_dialog = WFilter(self.axis, self.indices) + self.current_dialog.refreshNeeded.connect(self.update_indices) + self.current_dialog.show() + + def update_indices(self): + self.indices = self.current_dialog.indices + self.refreshNeeded.emit() + def set_name(self, name): """Method that set the name of the axis of the WSliceOperator Parameters @@ -188,37 +197,40 @@ def set_slider_floatedit(self): self : WSliceOperator a WSliceOperator object """ - # Converting the axis from rad to degree if the axis is angle as we do slice in degrees - # Recovering the value from the axis as well - if self.c_operation.currentText() == "slice": - if self.axis.name in ifft_dict: - operation = self.axis.name + "_to_" + self.axis_name - else: - operation = None - if self.axis_name == "angle": - self.axis_value = self.axis.get_values( - unit="°", operation=operation, corr_unit="rad", is_full=True - ) - self.unit = "°" - else: - self.axis_value = self.axis.get_values( - operation=operation, is_full=True - ) - elif self.c_operation.currentText() == "slice (fft)": - if self.axis.name == "angle": - self.axis_value = self.axis.get_values(operation="angle_to_wavenumber") - elif self.axis.name == "time": - self.axis_value = self.axis.get_values(operation="time_to_freqs") - else: # already wavenumber of freqs case - self.axis_value = self.axis.get_values() - - # Setting the initial value of the floatEdit to the minimum inside the axis - self.lf_value.setValue(min(self.axis_value)) - - # Setting the slider by giving the number of index according to the size of the axis - self.slider.setMinimum(0) - self.slider.setMaximum(len(self.axis_value) - 1) - self.slider.setValue(0) + if not self.axis.is_components: + # Converting the axis from rad to degree if the axis is angle as we do slice in degrees + # Recovering the value from the axis as well + if self.c_operation.currentText() == "slice": + if self.axis.name in ifft_dict: + operation = self.axis.name + "_to_" + self.axis_name + else: + operation = None + if self.axis_name == "angle": + self.axis_value = self.axis.get_values( + unit="°", operation=operation, corr_unit="rad", is_full=True + ) + self.unit = "°" + else: + self.axis_value = self.axis.get_values( + operation=operation, is_full=True + ) + elif self.c_operation.currentText() == "slice (fft)": + if self.axis.name == "angle": + self.axis_value = self.axis.get_values( + operation="angle_to_wavenumber" + ) + elif self.axis.name == "time": + self.axis_value = self.axis.get_values(operation="time_to_freqs") + else: # already wavenumber of freqs case + self.axis_value = self.axis.get_values() + + # Setting the initial value of the floatEdit to the minimum inside the axis + self.lf_value.setValue(min(self.axis_value)) + + # Setting the slider by giving the number of index according to the size of the axis + self.slider.setMinimum(0) + self.slider.setMaximum(len(self.axis_value) - 1) + self.slider.setValue(0) def update(self, axis): """Method that will update the WSliceOperator widget according to the axis given to it @@ -243,8 +255,7 @@ def update(self, axis): # Remove slice for string axes if self.axis.is_overlay: operation_list.remove("slice") - elif not self.axis.is_components: - operation_list.remove("overlay") + else: self.set_slider_floatedit() # Remove fft slice for non fft axes @@ -253,9 +264,11 @@ def update(self, axis): self.c_operation.clear() self.c_operation.addItems(operation_list) - if self.axis.is_components: - self.c_operation.setCurrentIndex(operation_list.index("overlay")) self.update_layout() + if self.axis.is_overlay: + self.c_operation.setCurrentIndex(operation_list.index("overlay")) + self.b_action.show() + self.b_action.setText("Overlay") self.c_operation.blockSignals(False) def update_floatEdit(self, is_refresh=True): @@ -296,8 +309,8 @@ def update_layout(self): elif extraction_selected == "overlay": self.lf_value.hide() self.slider.hide() - # self.b_action.show() - # self.b_action.setText(extraction_selected) + self.b_action.show() + self.b_action.setText("Overlay") self.refreshNeeded.emit() else: self.lf_value.hide() diff --git a/SciDataTool/Generator/ClassesRef/Data1D.csv b/SciDataTool/Generator/ClassesRef/Data1D.csv index 43198e88..1ef8b11c 100644 --- a/SciDataTool/Generator/ClassesRef/Data1D.csv +++ b/SciDataTool/Generator/ClassesRef/Data1D.csv @@ -3,5 +3,7 @@ values,,List or ndarray of the axis values,,ndarray,None,,,,,Data,get_values,VER is_components,,True if the axis values are strings,,bool,False,,,,,,get_length,,,, symmetries,,"Dictionary of the symmetries along each axis, used to reduce storage",,dict,{},,,,,,get_axis_periodic,,,, is_overlay,,True if axis must be used to overlay curves in plots,,bool,False,,,,,,has_period,,,, -,,,,,,,,,,,get_periodicity,,,, -,,,,,,,,,,,to_linspace,,,, +delimiter,,"Character used to separate attributes in string case (e.g. ""r=0, stator, radial"")",,str,None,,,,,,get_periodicity,,,, +sort_indices,,List of indices to use to sort axis,,list,None,,,,,,to_linspace,,,, +filter,,Dict of filter keys,,dict,None,,,,,,get_filter,,,, +,,,,,,,,,,,check_filter,,,, diff --git a/SciDataTool/Methods/Data1D/check_filter.py b/SciDataTool/Methods/Data1D/check_filter.py new file mode 100644 index 00000000..8e458994 --- /dev/null +++ b/SciDataTool/Methods/Data1D/check_filter.py @@ -0,0 +1,30 @@ +from SciDataTool.Functions import AxisError + + +def check_filter(self): + """Check if filter is correctly defined + Parameters + ---------- + self: Data1D + a Data1D object + Returns + ------- + keys: list + list of filter keys in same order as in values + """ + keys = [] + for i, value in enumerate(self.values): + items = value.split(self.delimiter) + if len(items) != len(self.filter.keys()): + raise AxisError("Filter is not correctly defined") + for item in items: + is_match = False + for key in self.filter: + if item in self.filter[key]: + is_match = True + if i == 0: + keys.append(key) + if not is_match: + raise AxisError("Missing item in filter: " + item) + + return keys diff --git a/SciDataTool/Methods/Data1D/get_filter.py b/SciDataTool/Methods/Data1D/get_filter.py new file mode 100644 index 00000000..21ee0ba6 --- /dev/null +++ b/SciDataTool/Methods/Data1D/get_filter.py @@ -0,0 +1,38 @@ +from SciDataTool.Functions import AxisError + + +def get_filter(self, filter_dict): + """Get filtering indices + Parameters + ---------- + self: Data1D + a Data1D object + filter_dict: dict + a dict of the values to filter (keep values defined in filter_dict) + Returns + ------- + indices: list + list of indices to use to get corresponding filter + """ + + # Check that keys in filter_dict are correctly defined in self.filter + if len(filter_dict.keys()) != len(self.filter.keys()): + raise AxisError("Filter key not defined in Data1D.filter") + for key in filter_dict: + if key not in self.filter: + raise AxisError("Filter key not defined in Data1D.filter") + + # Prepare filtering keys in correct order + keys = self.check_filter() + + indices = [] + for i, value in enumerate(self.values): + items = value.split(self.delimiter) + nb_match = 0 + for j, item in enumerate(items): + if item in filter_dict[keys[j]]: + nb_match += 1 + if nb_match == len(keys): + indices.append(i) + + return indices diff --git a/SciDataTool/Methods/DataND/plot.py b/SciDataTool/Methods/DataND/plot.py index 4af1ac70..0e8ee6e0 100644 --- a/SciDataTool/Methods/DataND/plot.py +++ b/SciDataTool/Methods/DataND/plot.py @@ -43,7 +43,7 @@ def plot( is_create_appli : bool True to create an QApplication (required if not already created by another GUI) frozen_type : int - 0 to let the user modify the axis of the plot, 1 to let him switch them, 2 to not let him change them, 3 to freeze both axes and operations + 0 to let the user modify the axis of the plot, 1 to let him switch them, 2 to not let him change them, 3 to freeze both axes and operations, 4 to freeze fft """ if is_create_appli: @@ -54,6 +54,9 @@ def plot( [arg for arg in args if arg != None], axis_data=None ) + if unit is None: + unit = self.unit + wid = DDataPlotter( data=self, axes_request_list=axes_request_list, diff --git a/SciDataTool/Methods/DataND/plot_3D_Data.py b/SciDataTool/Methods/DataND/plot_3D_Data.py index 006a3610..7a4746f5 100644 --- a/SciDataTool/Methods/DataND/plot_3D_Data.py +++ b/SciDataTool/Methods/DataND/plot_3D_Data.py @@ -255,7 +255,7 @@ def plot_3D_Data( xticks = [i * round(np_max(axis.values) / 6) for i in range(7)] else: xticks = None - if axis.is_components and axis.extension != "list": + if axis.is_components: xticklabels = result[axis.name] xticks = Xdata if annotation_delim is not None and annotation_delim in xticklabels[0]: @@ -293,7 +293,7 @@ def plot_3D_Data( yticks = [i * round(np_max(axis.values) / 6) for i in range(7)] else: yticks = None - if axis.is_components and axis.extension != "list": + if axis.is_components: yticklabels = result[axis.name] yticks = Ydata if annotation_delim is not None and annotation_delim in yticklabels[0]: diff --git a/SciDataTool/Methods/VectorField/plot.py b/SciDataTool/Methods/VectorField/plot.py index 550a7bf3..7827b071 100644 --- a/SciDataTool/Methods/VectorField/plot.py +++ b/SciDataTool/Methods/VectorField/plot.py @@ -42,7 +42,7 @@ def plot( is_create_appli : bool True to create an QApplication (required if not already created by another GUI) frozen_type : int - 0 to let the user modify the axis of the plot, 1 to let him switch them, 2 to not let him change them, 3 to freeze both axes and operations + 0 to let the user modify the axis of the plot, 1 to let him switch them, 2 to not let him change them, 3 to freeze both axes and operations, 4 to freeze fft """ if is_create_appli: diff --git a/Tests/GUI/UI/test_filter_widget.py b/Tests/GUI/UI/test_filter_widget.py new file mode 100644 index 00000000..5f0233b6 --- /dev/null +++ b/Tests/GUI/UI/test_filter_widget.py @@ -0,0 +1,78 @@ +import pytest +from PySide2 import QtWidgets +from PySide2.QtCore import Qt +import sys +from Tests.GUI import Field_filter +from SciDataTool.GUI.WFilter.WFilter import WFilter + + +class TestGUI(object): + @classmethod + def setup_class(cls): + """Run at the begining of every test to setup the gui""" + if not QtWidgets.QApplication.instance(): + cls.app = QtWidgets.QApplication(sys.argv) + else: + cls.app = QtWidgets.QApplication.instance() + + cls.UI = Field_filter.plot( + "time", "loadcases[]", is_show_fig=False, is_create_appli=False + ) + + @pytest.mark.gui + def check_filter_buttons(self): + axis_1 = self.UI.w_plot_manager.w_axis_manager.w_axis_1 + assert axis_1.b_filter.isHidden() + slice_op = self.UI.w_plot_manager.w_axis_manager.w_slice_op[0] + assert not slice_op.b_action.isHidden() + + @pytest.mark.gui + def check_filter_table_init(self): + # Check that filter table initializes correctly + self.UI.w_plot_manager.w_axis_manager.w_slice_op[0].b_action.clicked.emit() + wfilter = self.UI.w_plot_manager.w_axis_manager.w_slice_op[0].current_dialog + assert isinstance(wfilter, WFilter) + assert wfilter.indices is None + wfilter.b_Ok.clicked.emit() + assert wfilter.indices == [i for i in range(12)] + assert self.UI.w_plot_manager.w_axis_manager.w_slice_op[0].indices == [ + i for i in range(12) + ] + # Select 3 indices and check table initialization + self.UI.w_plot_manager.w_axis_manager.w_slice_op[0].indices = [0, 2, 5] + self.UI.w_plot_manager.w_axis_manager.w_slice_op[0].b_action.clicked.emit() + wfilter = self.UI.w_plot_manager.w_axis_manager.w_slice_op[0].current_dialog + assert isinstance(wfilter, WFilter) + assert wfilter.indices == [0, 2, 5] + + @pytest.mark.gui + def check_filter_table_manip(self): + self.UI.w_plot_manager.w_axis_manager.w_slice_op[0].b_action.clicked.emit() + wfilter = self.UI.w_plot_manager.w_axis_manager.w_slice_op[0].current_dialog + assert isinstance(wfilter, WFilter) + assert wfilter.indices is None + # Uncheck one line and check indices + wfilter.tab_indices.model().data( + wfilter.tab_indices.model().index( + 2, wfilter.tab_indices.model().columnCount() - 1 + ) + ).setCheckState(Qt.CheckState(False)) + wfilter.b_Ok.clicked.emit() + assert wfilter.indices == [i for i in range(12) if i != 2] + # Sort by first column and check indices + self.UI.w_plot_manager.w_axis_manager.w_slice_op[0].b_action.clicked.emit() + wfilter = self.UI.w_plot_manager.w_axis_manager.w_slice_op[0].current_dialog + assert isinstance(wfilter, WFilter) + wfilter.tab_indices.model().sort(0, Qt.AscendingOrder) + wfilter.b_Ok.clicked.emit() + assert list(set(wfilter.indices)) == [i for i in range(12) if i != 2] + + +if __name__ == "__main__": + a = TestGUI() + a.setup_class() + # a.check_filter_buttons() + # a.check_filter_table_init() + a.check_filter_table_manip() + + print("Done") diff --git a/Tests/GUI/__init__.py b/Tests/GUI/__init__.py index d6534373..c01e7778 100644 --- a/Tests/GUI/__init__.py +++ b/Tests/GUI/__init__.py @@ -1,6 +1,6 @@ -from SciDataTool import DataLinspace, DataTime +from SciDataTool import DataLinspace, DataTime, Data1D from numpy.random import random -from numpy import pi +from numpy import pi, zeros f = 50 @@ -20,3 +20,41 @@ axes=[Time, Angle, Z], values=field, ) + +X = DataLinspace(name="time", unit="s", initial=0, final=1, number=11) +Y = Data1D( + name="loadcases", + unit="", + values=[ + "r=0, radial, stator", + "r=-2, radial, stator", + "r=2, radial, stator", + "r=0, circumferential, stator", + "r=-2, circumferential, stator", + "r=2, circumferential, stator", + "r=0, radial, rotor", + "r=-2, radial, rotor", + "r=2, radial, rotor", + "r=0, circumferential, rotor", + "r=-2, circumferential, rotor", + "r=2, circumferential, rotor", + ], + is_components=True, + delimiter=", ", + filter={ + "wavenumber": ["r=0", "r=-2", "r=2"], + "direction": ["radial", "circumferential"], + "application": ["stator", "rotor"], + }, + is_overlay=True, +) +field_filter = zeros((11, 12)) +for i in range(12): + field_filter[:, i] = i +Field_filter = DataTime( + name="Airgap flux density", + symbol="B_r", + unit="T", + axes=[X, Y], + values=field_filter, +) diff --git a/Tests/GUI/dev_GUI.py b/Tests/GUI/dev_GUI.py index 6b5c9c7e..a93e98d8 100644 --- a/Tests/GUI/dev_GUI.py +++ b/Tests/GUI/dev_GUI.py @@ -24,8 +24,8 @@ values=field_3d, ) - # test = "plot" - test = "autoplot" + test = "plot" + # test = "autoplot" # test = "plot_2axis" # test = "oneaxis" # test = "vect" diff --git a/Tests/Validation/test_filter.py b/Tests/Validation/test_filter.py new file mode 100644 index 00000000..ed1c2b38 --- /dev/null +++ b/Tests/Validation/test_filter.py @@ -0,0 +1,111 @@ +import pytest +import numpy as np + +from SciDataTool import Data1D, DataLinspace, DataTime + + +@pytest.mark.validation +def test_filter_meth(): + X = Data1D( + name="loadcases", + unit="", + values=[ + "r=0, radial, stator", + "r=-2, radial, stator", + "r=2, radial, stator", + "r=0, circumferential, stator", + "r=-2, circumferential, stator", + "r=2, circumferential, stator", + "r=0, radial, rotor", + "r=-2, radial, rotor", + "r=2, radial, rotor", + "r=0, circumferential, rotor", + "r=-2, circumferential, rotor", + "r=2, circumferential, rotor", + ], + is_components=True, + delimiter=", ", + filter={ + "wavenumber": ["r=0", "r=-2", "r=2"], + "direction": ["radial", "circumferential"], + "application": ["stator", "rotor"], + }, + ) + indices = X.get_filter( + filter_dict={ + "wavenumber": ["r=0"], + "direction": ["radial", "circumferential"], + "application": ["stator", "rotor"], + } + ) + assert indices == [0, 3, 6, 9] + indices = X.get_filter( + filter_dict={ + "wavenumber": ["r=0", "r=2"], + "direction": ["radial"], + "application": ["stator", "rotor"], + } + ) + assert indices == [0, 2, 6, 8] + indices = X.get_filter( + filter_dict={ + "wavenumber": ["r=0", "r=2", "r=-2"], + "direction": ["radial", "circumferential"], + "application": ["rotor"], + } + ) + assert indices == [6, 7, 8, 9, 10, 11] + + +@pytest.mark.validation +def test_filter_field(): + X = DataLinspace(name="time", unit="s", initial=0, final=10, number=11) + Y = Data1D( + name="loadcases", + unit="", + values=[ + "r=0, radial, stator", + "r=-2, radial, stator", + "r=2, radial, stator", + "r=0, circumferential, stator", + "r=-2, circumferential, stator", + "r=2, circumferential, stator", + "r=0, radial, rotor", + "r=-2, radial, rotor", + "r=2, radial, rotor", + "r=0, circumferential, rotor", + "r=-2, circumferential, rotor", + "r=2, circumferential, rotor", + ], + is_components=True, + delimiter=", ", + filter={ + "wavenumber": ["r=0", "r=-2", "r=2"], + "direction": ["radial", "circumferential"], + "application": ["stator", "rotor"], + }, + ) + field = np.zeros((11, 12)) + Field = DataTime( + name="Airgap flux density", + symbol="B_r", + unit="T", + axes=[X, Y], + values=field, + ) + + indices = Y.get_filter( + filter_dict={ + "wavenumber": ["r=0"], + "direction": ["radial", "circumferential"], + "application": ["stator", "rotor"], + } + ) + assert indices == [0, 3, 6, 9] + result = Field.get_along("time", "loadcases" + str(indices)) + assert result[Field.symbol].shape == (11, 4) + + +if __name__ == "__main__": + test_filter_meth() + test_filter_field() \ No newline at end of file