source: sasview/src/sas/qtgui/Calculators/DataOperationUtilityPanel.py @ aefde77

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since aefde77 was e90988c, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Show help pages in default browser. Fixed some help links and modified unit tests. SASVIEW-800

  • Property mode set to 100644
File size: 16.3 KB
RevLine 
[f0bb711]1import time
2import logging
3import re
4import copy
5
[4992ff2]6from PyQt5 import QtCore
7from PyQt5 import QtGui
8from PyQt5 import QtWidgets
[d5c5d3d]9
10from sas.qtgui.Plotting.PlotterData import Data1D
11from sas.qtgui.Plotting.Plotter import PlotterWidget
12from sas.qtgui.Plotting.PlotterData import Data2D
13from sas.qtgui.Plotting.Plotter2D import Plotter2DWidget
14import sas.qtgui.Utilities.GuiUtils as GuiUtils
15
[b3e8629]16from .UI.DataOperationUtilityUI import Ui_DataOperationUtility
[d5c5d3d]17
18BG_WHITE = "background-color: rgb(255, 255, 255);"
19BG_RED = "background-color: rgb(244, 170, 164);"
20
[f0bb711]21
[4992ff2]22class DataOperationUtilityPanel(QtWidgets.QDialog, Ui_DataOperationUtility):
[d5c5d3d]23    def __init__(self, parent=None):
24        super(DataOperationUtilityPanel, self).__init__()
25        self.setupUi(self)
26        self.manager = parent
27        self.communicator = self.manager.communicator()
28
29        # To store input datafiles
30        self.filenames = None
31        self.list_data_items = []
32        self.data1 = None
33        self.data2 = None
34        # To store the result
35        self.output = None
36
37        # To update content of comboboxes with files loaded in DataExplorer
[f0bb711]38        self.communicator.sendDataToPanelSignal.connect(self.updateCombobox)
[d5c5d3d]39
40        # change index of comboboxes
41        self.cbData1.currentIndexChanged.connect(self.onSelectData1)
42        self.cbData2.currentIndexChanged.connect(self.onSelectData2)
43        self.cbOperator.currentIndexChanged.connect(self.onSelectOperator)
44
45        # edit Coefficient text edit
46        self.txtNumber.textChanged.connect(self.onInputCoefficient)
47        self.txtOutputData.textChanged.connect(self.onCheckOutputName)
48
49        # push buttons
[f0bb711]50        self.cmdClose.clicked.connect(self.onClose)
[d5c5d3d]51        self.cmdHelp.clicked.connect(self.onHelp)
52        self.cmdCompute.clicked.connect(self.onCompute)
53        self.cmdReset.clicked.connect(self.onReset)
54
55        self.cmdCompute.setEnabled(False)
56
57        # validator for coefficient
[d6b8a1d]58        self.txtNumber.setValidator(GuiUtils.DoubleValidator())
[d5c5d3d]59
[4992ff2]60        self.layoutOutput = QtWidgets.QHBoxLayout()
61        self.layoutData1 = QtWidgets.QHBoxLayout()
62        self.layoutData2 = QtWidgets.QHBoxLayout()
[d5c5d3d]63
[f0bb711]64        # Create default layout for initial graphs (when they are still empty)
[d5c5d3d]65        self.newPlot(self.graphOutput, self.layoutOutput)
66        self.newPlot(self.graphData1, self.layoutData1)
67        self.newPlot(self.graphData2, self.layoutData2)
68
69        # Flag to enable Compute pushbutton
70        self.data2OK = False
71        self.data1OK = False
72
73    def updateCombobox(self, filenames):
74        """ Function to fill comboboxes with names of datafiles loaded in
75         DataExplorer. For Data2, there is the additional option of choosing
76         a number to apply to data1 """
77        self.filenames = filenames
78
[b3e8629]79        if list(filenames.keys()):
[d5c5d3d]80            # clear contents of comboboxes
81            self.cbData1.clear()
82            self.cbData1.addItems(['Select Data'])
83            self.cbData2.clear()
84            self.cbData2.addItems(['Select Data', 'Number'])
85
86            list_datafiles = []
87
[b3e8629]88            for key_id in list(filenames.keys()):
[f0bb711]89                if filenames[key_id].get_data().title:
[d5c5d3d]90                    # filenames with titles
[f0bb711]91                    new_title = filenames[key_id].get_data().title
[d5c5d3d]92                    list_datafiles.append(new_title)
93                    self.list_data_items.append(new_title)
94
95                else:
96                    # filenames without titles by removing time.time()
[f0bb711]97                    new_title = re.sub('\d{10}\.\d{2}', '', str(key_id))
[d5c5d3d]98                    self.list_data_items.append(new_title)
99                    list_datafiles.append(new_title)
100
101            # update contents of comboboxes
102            self.cbData1.addItems(list_datafiles)
103            self.cbData2.addItems(list_datafiles)
104
105    def onHelp(self):
106        """
107        Bring up the Data Operation Utility Documentation whenever
108        the HELP button is clicked.
109        Calls Documentation Window with the path of the location within the
110        documentation tree (after /doc/ ....".
111        """
[e90988c]112        location = "/user/sasgui/perspectives/calculator/data_operator_help.html"
113        self.manager.showHelp(location)
[d5c5d3d]114
115    def onClose(self):
116        """ Close dialog """
117        self.onReset()
[1420066]118
119        self.cbData1.clear()
120        self.cbData1.addItems(['No Data Available'])
121        self.cbData2.clear()
122        self.cbData2.addItems(['No Data Available'])
[d5c5d3d]123        self.close()
124
[1420066]125
[d5c5d3d]126    def onCompute(self):
127        """ perform calculation """
[0c468bf]128        # set operator to be applied
129        operator = self.cbOperator.currentText()
130        # calculate and send data to DataExplorer
131        output = None
132        try:
133            data1 = self.data1
134            data2 = self.data2
[fbfc488]135            output = eval("data1 %s data2" % operator)
136        except Exception as ex:
137            logging.error(ex)
138            return
[0c468bf]139
140        self.output = output
141
142        # if outputname was unused, write output result to it
143        # and display plot
144        if self.onCheckOutputName():
145            # add outputname to self.filenames
146            self.list_data_items.append(str(self.txtOutputData.text()))
147            # send result to DataExplorer
148            self.onPrepareOutputData()
149            # plot result
150            self.updatePlot(self.graphOutput, self.layoutOutput, self.output)
[d5c5d3d]151
152    def onPrepareOutputData(self):
153        """ Prepare datasets to be added to DataExplorer and DataManager """
154        new_item = GuiUtils.createModelItemWithPlot(
[cee5c78]155            self.output,
[d5c5d3d]156            name=self.txtOutputData.text())
157
158        new_datalist_item = {str(self.txtOutputData.text()) + str(time.time()):
159                                 self.output}
160        self.communicator. \
161            updateModelFromDataOperationPanelSignal.emit(new_item, new_datalist_item)
162
163    def onSelectOperator(self):
[f0bb711]164        """ Change GUI when operator changed """
[d5c5d3d]165        self.lblOperatorApplied.setText(self.cbOperator.currentText())
166        self.newPlot(self.graphOutput, self.layoutOutput)
167
168    def onReset(self):
169        """
170        Reset Panel to its initial state (default values) keeping
171        the names of loaded data
172        """
173        self.txtNumber.setText('1.0')
174        self.txtOutputData.setText('MyNewDataName')
175
176        self.txtNumber.setEnabled(False)
177        self.cmdCompute.setEnabled(False)
178
179        self.cbData1.setCurrentIndex(0)
180        self.cbData2.setCurrentIndex(0)
181        self.cbOperator.setCurrentIndex(0)
182
183        self.output = None
184        self.data1 = None
185        self.data2 = None
[f0bb711]186        self.filenames = None
187        self.list_data_items = []
[d5c5d3d]188
189        self.data1OK = False
190        self.data2OK = False
191
192        # Empty graphs
193        self.newPlot(self.graphOutput, self.layoutOutput)
194        self.newPlot(self.graphData1, self.layoutData1)
195        self.newPlot(self.graphData2, self.layoutData2)
196
197    def onSelectData1(self):
198        """ Plot for selection of Data1 """
199        choice_data1 = str(self.cbData1.currentText())
200
201        wrong_choices = ['No Data Available', 'Select Data', '']
202
203        if choice_data1 in wrong_choices:
204            # check validity of choice: input = filename
205            self.newPlot(self.graphData1, self.layoutData1)
206            self.data1 = None
207            self.data1OK = False
[0c468bf]208            self.cmdCompute.setEnabled(False) # self.onCheckChosenData())
[d5c5d3d]209            return
210
211        else:
212            self.data1OK = True
213            # get Data1
[f0bb711]214            key_id1 = self._findId(choice_data1)
215            self.data1 = self._extractData(key_id1)
[d5c5d3d]216            # plot Data1
217            self.updatePlot(self.graphData1, self.layoutData1, self.data1)
218            # plot default for output graph
219            self.newPlot(self.graphOutput, self.layoutOutput)
[0c468bf]220            # Enable Compute button only if Data2 is defined and data compatible
221            self.cmdCompute.setEnabled(self.onCheckChosenData())
[f0bb711]222
[d5c5d3d]223    def onSelectData2(self):
224        """ Plot for selection of Data2 """
225        choice_data2 = str(self.cbData2.currentText())
226        wrong_choices = ['No Data Available', 'Select Data', '']
227
228        if choice_data2 in wrong_choices:
229            self.newPlot(self.graphData2, self.layoutData2)
230            self.txtNumber.setEnabled(False)
231            self.data2OK = False
[f0bb711]232            self.onCheckChosenData()
[0c468bf]233            self.cmdCompute.setEnabled(False)
[d5c5d3d]234            return
235
236        elif choice_data2 == 'Number':
237            self.data2OK = True
238            self.txtNumber.setEnabled(True)
239            self.data2 = float(self.txtNumber.text())
240
[0c468bf]241            # Enable Compute button only if Data1 defined and compatible data
242            self.cmdCompute.setEnabled(self.onCheckChosenData())
[d5c5d3d]243            # Display value of coefficient in graphData2
244            self.updatePlot(self.graphData2, self.layoutData2, self.data2)
245            # plot default for output graph
246            self.newPlot(self.graphOutput, self.layoutOutput)
[f0bb711]247            self.onCheckChosenData()
[d5c5d3d]248
249        else:
250            self.txtNumber.setEnabled(False)
[0c468bf]251            self.data2OK = True
[f0bb711]252            key_id2 = self._findId(choice_data2)
253            self.data2 = self._extractData(key_id2)
[0c468bf]254            self.cmdCompute.setEnabled(self.onCheckChosenData())
255
[d5c5d3d]256            # plot Data2
257            self.updatePlot(self.graphData2, self.layoutData2, self.data2)
258            # plot default for output graph
259            self.newPlot(self.graphOutput, self.layoutOutput)
260
261    def onInputCoefficient(self):
262        """ Check input of number when a coefficient is required
263        for operation """
264        if self.txtNumber.isModified():
265            input_to_check = str(self.txtNumber.text())
266
267            if input_to_check is None or input_to_check is '':
268                msg = 'DataOperation: Number requires a float number'
[7d9c83c]269                logging.warning(msg)
[7fb471d]270                self.txtNumber.setStyleSheet(BG_RED)
[d5c5d3d]271
272            elif float(self.txtNumber.text()) == 0.:
273                # should be check that 0 is not chosen
274                msg = 'DataOperation: Number requires a non zero number'
[7d9c83c]275                logging.warning(msg)
[7fb471d]276                self.txtNumber.setStyleSheet(BG_RED)
[d5c5d3d]277
278            else:
[7fb471d]279                self.txtNumber.setStyleSheet(BG_WHITE)
[d5c5d3d]280                self.data2 = float(self.txtNumber.text())
281                self.updatePlot(self.graphData2, self.layoutData2, self.data2)
282
283    def onCheckChosenData(self):
[0c468bf]284        """ check that data1 and data2 are compatible """
[d5c5d3d]285
[0c468bf]286        if not all([self.data1OK, self.data2OK]):
[d5c5d3d]287            return False
288        else:
[0c468bf]289            if self.cbData2.currentText() == 'Number':
[7fb471d]290                self.cbData1.setStyleSheet(BG_WHITE)
291                self.cbData2.setStyleSheet(BG_WHITE)
[0c468bf]292                return True
293
294            elif self.data1.__class__.__name__ != self.data2.__class__.__name__:
[7fb471d]295                self.cbData1.setStyleSheet(BG_RED)
296                self.cbData2.setStyleSheet(BG_RED)
[b3e8629]297                print(self.data1.__class__.__name__ != self.data2.__class__.__name__)
[7d9c83c]298                logging.warning('Cannot compute data of different dimensions')
[0c468bf]299                return False
300
301            elif self.data1.__class__.__name__ == 'Data1D'\
302                    and (len(self.data2.x) != len(self.data1.x) or
303                             not all(i == j for i, j in zip(self.data1.x, self.data2.x))):
[7d9c83c]304                logging.warning('Cannot compute 1D data of different lengths')
[7fb471d]305                self.cbData1.setStyleSheet(BG_RED)
306                self.cbData2.setStyleSheet(BG_RED)
[0c468bf]307                return False
308
309            elif self.data1.__class__.__name__ == 'Data2D' \
310                    and (len(self.data2.qx_data) != len(self.data1.qx_data) \
311                    or len(self.data2.qy_data) != len(self.data1.qy_data)
312                    or not all(i == j for i, j in
313                                     zip(self.data1.qx_data, self.data2.qx_data))
314                    or not all(i == j for i, j in
315                                zip(self.data1.qy_data, self.data2.qy_data))
316                         ):
[7fb471d]317                self.cbData1.setStyleSheet(BG_RED)
318                self.cbData2.setStyleSheet(BG_RED)
[7d9c83c]319                logging.warning('Cannot compute 2D data of different lengths')
[0c468bf]320                return False
321
322            else:
[7fb471d]323                self.cbData1.setStyleSheet(BG_WHITE)
324                self.cbData2.setStyleSheet(BG_WHITE)
[0c468bf]325                return True
[d5c5d3d]326
327    def onCheckOutputName(self):
328        """ Check that name of output does not already exist """
329        name_to_check = str(self.txtOutputData.text())
[7fb471d]330        self.txtOutputData.setStyleSheet(BG_WHITE)
[d5c5d3d]331
332        if name_to_check is None or name_to_check == '':
[7fb471d]333            self.txtOutputData.setStyleSheet(BG_RED)
[7d9c83c]334            logging.warning('No output name')
[d5c5d3d]335            return False
336
337        elif name_to_check in self.list_data_items:
[7fb471d]338            self.txtOutputData.setStyleSheet(BG_RED)
[7d9c83c]339            logging.warning('The Output data name already exists')
[d5c5d3d]340            return False
341
342        else:
[7fb471d]343            self.txtOutputData.setStyleSheet(BG_WHITE)
[d5c5d3d]344            return True
345
346    # ########
347    # Modification of inputs
348    # ########
349    def _findId(self, name):
350        """ find id of name in list of filenames """
[b3e8629]351        isinstance(name, str)
[d5c5d3d]352
[b3e8629]353        for key_id in list(self.filenames.keys()):
[d5c5d3d]354            # data with title
[f0bb711]355            if self.filenames[key_id].get_data().title:
356                input = self.filenames[key_id].get_data().title
[d5c5d3d]357            # data without title
358            else:
[f0bb711]359                input = str(key_id)
[d5c5d3d]360            if name in input:
[f0bb711]361                return key_id
[d5c5d3d]362
[f0bb711]363    def _extractData(self, key_id):
[d5c5d3d]364        """ Extract data from file with id contained in list of filenames """
[f0bb711]365        data_complete = self.filenames[key_id].get_data()
[d5c5d3d]366        dimension = data_complete.__class__.__name__
367
[f0bb711]368        if dimension in ('Data1D', 'Data2D'):
369            return copy.deepcopy(data_complete)
[d5c5d3d]370
371        else:
[7d9c83c]372            logging.warning('Error with data format')
[d5c5d3d]373            return
374
375    # ########
376    # PLOTS
377    # ########
378    def newPlot(self, graph, layout):
379        """ Create template for graphs with default '?' layout"""
[4992ff2]380        assert isinstance(graph, QtWidgets.QGraphicsView)
381        assert isinstance(layout, QtWidgets.QHBoxLayout)
[d5c5d3d]382
383        # clear layout
384        if layout.count() > 0:
385            item = layout.takeAt(0)
386            layout.removeItem(item)
387
388        layout.setContentsMargins(0, 0, 0, 0)
[f0bb711]389        layout.addWidget(self.prepareSubgraphWithData("?"))
[d5c5d3d]390
391        graph.setLayout(layout)
392
393    def updatePlot(self, graph, layout, data):
394        """ plot data in graph after clearing its layout """
395
[4992ff2]396        assert isinstance(graph, QtWidgets.QGraphicsView)
397        assert isinstance(layout, QtWidgets.QHBoxLayout)
[d5c5d3d]398
399        # clear layout
400        if layout.count() > 0:
401            item = layout.takeAt(0)
402            layout.removeItem(item)
403
404        layout.setContentsMargins(0, 0, 0, 0)
405
406        if isinstance(data, Data2D):
407            # plot 2D data
408            plotter2D = Plotter2DWidget(self, quickplot=True)
409            plotter2D.data = data
410            plotter2D.scale = 'linear'
411
412            plotter2D.ax.tick_params(axis='x', labelsize=8)
413            plotter2D.ax.tick_params(axis='y', labelsize=8)
414
415            # Draw zero axis lines.
416            plotter2D.ax.axhline(linewidth=1)
417            plotter2D.ax.axvline(linewidth=1)
418
419            graph.setLayout(layout)
420            layout.addWidget(plotter2D)
421            # remove x- and ylabels
422            plotter2D.y_label = ''
423            plotter2D.x_label = ''
424            plotter2D.plot(show_colorbar=False)
425            plotter2D.show()
426
427        elif isinstance(data, Data1D):
428            # plot 1D data
429            plotter = PlotterWidget(self, quickplot=True)
430            plotter.data = data
431
432            graph.setLayout(layout)
433            layout.addWidget(plotter)
434
435            plotter.ax.tick_params(axis='x', labelsize=8)
436            plotter.ax.tick_params(axis='y', labelsize=8)
437
[f0bb711]438            plotter.plot(hide_error=True, marker='.')
439            # plotter.legend = None
[d5c5d3d]440
441            plotter.show()
442
443        elif float(data) and self.cbData2.currentText() == 'Number':
444            # display value of coefficient (to be applied to Data1)
445            # in graphData2
[f0bb711]446            layout.addWidget(self.prepareSubgraphWithData(data))
[d5c5d3d]447
[f0bb711]448            graph.setLayout(layout)
[d5c5d3d]449
[f0bb711]450    def prepareSubgraphWithData(self, data):
451        """ Create graphics view containing scene with string """
[4992ff2]452        scene = QtWidgets.QGraphicsScene()
[f0bb711]453        scene.addText(str(data))
[d5c5d3d]454
[4992ff2]455        subgraph = QtWidgets.QGraphicsView()
[f0bb711]456        subgraph.setScene(scene)
457
458        return subgraph
Note: See TracBrowser for help on using the repository browser.