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

Last change on this file since 5a2bb75 was 42787fb, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Fixing issues with DataOperationUtility? calculator

  • Property mode set to 100644
File size: 16.2 KB
Line 
1import time
2import logging
3import re
4import copy
5
6from PyQt5 import QtCore
7from PyQt5 import QtGui
8from PyQt5 import QtWidgets
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
16from .UI.DataOperationUtilityUI import Ui_DataOperationUtility
17
18BG_WHITE = "background-color: rgb(255, 255, 255);"
19BG_RED = "background-color: rgb(244, 170, 164);"
20
21
22class DataOperationUtilityPanel(QtWidgets.QDialog, Ui_DataOperationUtility):
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
38        self.communicator.sendDataToPanelSignal.connect(self.updateCombobox)
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
50        self.cmdClose.clicked.connect(self.onClose)
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
58        self.txtNumber.setValidator(GuiUtils.DoubleValidator())
59
60        self.layoutOutput = QtWidgets.QHBoxLayout()
61        self.layoutData1 = QtWidgets.QHBoxLayout()
62        self.layoutData2 = QtWidgets.QHBoxLayout()
63
64        # Create default layout for initial graphs (when they are still empty)
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
79        if list(filenames.keys()):
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
88            for key_id in list(filenames.keys()):
89                if filenames[key_id].get_data().title:
90                    # filenames with titles
91                    new_title = filenames[key_id].get_data().title
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()
97                    new_title = re.sub('\d{10}\.\d{2}', '', str(key_id))
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        """
112        location = "/user/qtgui/Calculators/data_operator_help.html"
113        self.manager.showHelp(location)
114
115    def onClose(self):
116        """ Close dialog """
117        self.onReset()
118
119        self.cbData1.clear()
120        self.cbData1.addItems(['No Data Available'])
121        self.cbData2.clear()
122        self.cbData2.addItems(['No Data Available'])
123        self.close()
124
125
126    def onCompute(self):
127        """ perform calculation """
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
135            output = eval("data1 %s data2" % operator)
136        except Exception as ex:
137            logging.error(ex)
138            return
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)
151
152    def onPrepareOutputData(self):
153        """ Prepare datasets to be added to DataExplorer and DataManager """
154        new_item = GuiUtils.createModelItemWithPlot(
155            self.output,
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):
164        """ Change GUI when operator changed """
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.data1OK = False
184        self.data2OK = False
185
186        # Empty graphs
187        self.newPlot(self.graphOutput, self.layoutOutput)
188        self.newPlot(self.graphData1, self.layoutData1)
189        self.newPlot(self.graphData2, self.layoutData2)
190
191    def onSelectData1(self):
192        """ Plot for selection of Data1 """
193        choice_data1 = str(self.cbData1.currentText())
194
195        wrong_choices = ['No Data Available', 'Select Data', '']
196
197        if choice_data1 in wrong_choices:
198            # check validity of choice: input = filename
199            self.newPlot(self.graphData1, self.layoutData1)
200            self.data1 = None
201            self.data1OK = False
202            self.cmdCompute.setEnabled(False) # self.onCheckChosenData())
203            return
204
205        else:
206            self.data1OK = True
207            # get Data1
208            key_id1 = self._findId(choice_data1)
209            self.data1 = self._extractData(key_id1)
210            # plot Data1
211            self.updatePlot(self.graphData1, self.layoutData1, self.data1)
212            # plot default for output graph
213            self.newPlot(self.graphOutput, self.layoutOutput)
214            # Enable Compute button only if Data2 is defined and data compatible
215            self.cmdCompute.setEnabled(self.onCheckChosenData())
216
217    def onSelectData2(self):
218        """ Plot for selection of Data2 """
219        choice_data2 = str(self.cbData2.currentText())
220        wrong_choices = ['No Data Available', 'Select Data', '']
221
222        if choice_data2 in wrong_choices:
223            self.newPlot(self.graphData2, self.layoutData2)
224            self.txtNumber.setEnabled(False)
225            self.data2OK = False
226            self.onCheckChosenData()
227            self.cmdCompute.setEnabled(False)
228            return
229
230        elif choice_data2 == 'Number':
231            self.data2OK = True
232            self.txtNumber.setEnabled(True)
233            self.data2 = float(self.txtNumber.text())
234
235            # Enable Compute button only if Data1 defined and compatible data
236            self.cmdCompute.setEnabled(self.onCheckChosenData())
237            # Display value of coefficient in graphData2
238            self.updatePlot(self.graphData2, self.layoutData2, self.data2)
239            # plot default for output graph
240            self.newPlot(self.graphOutput, self.layoutOutput)
241            self.onCheckChosenData()
242
243        else:
244            self.txtNumber.setEnabled(False)
245            self.data2OK = True
246            key_id2 = self._findId(choice_data2)
247            self.data2 = self._extractData(key_id2)
248            self.cmdCompute.setEnabled(self.onCheckChosenData())
249
250            # plot Data2
251            self.updatePlot(self.graphData2, self.layoutData2, self.data2)
252            # plot default for output graph
253            self.newPlot(self.graphOutput, self.layoutOutput)
254
255    def onInputCoefficient(self):
256        """ Check input of number when a coefficient is required
257        for operation """
258        if self.txtNumber.isModified():
259            input_to_check = str(self.txtNumber.text())
260
261            if input_to_check is None or input_to_check is '':
262                msg = 'DataOperation: Number requires a float number'
263                logging.warning(msg)
264                self.txtNumber.setStyleSheet(BG_RED)
265
266            elif float(self.txtNumber.text()) == 0.:
267                # should be check that 0 is not chosen
268                msg = 'DataOperation: Number requires a non zero number'
269                logging.warning(msg)
270                self.txtNumber.setStyleSheet(BG_RED)
271
272            else:
273                self.txtNumber.setStyleSheet(BG_WHITE)
274                self.data2 = float(self.txtNumber.text())
275                self.updatePlot(self.graphData2, self.layoutData2, self.data2)
276
277    def onCheckChosenData(self):
278        """ check that data1 and data2 are compatible """
279
280        if not all([self.data1OK, self.data2OK]):
281            return False
282        else:
283            if self.cbData2.currentText() == 'Number':
284                self.cbData1.setStyleSheet(BG_WHITE)
285                self.cbData2.setStyleSheet(BG_WHITE)
286                return True
287
288            elif self.data1.__class__.__name__ != self.data2.__class__.__name__:
289                self.cbData1.setStyleSheet(BG_RED)
290                self.cbData2.setStyleSheet(BG_RED)
291                print(self.data1.__class__.__name__ != self.data2.__class__.__name__)
292                logging.warning('Cannot compute data of different dimensions')
293                return False
294
295            elif self.data1.__class__.__name__ == 'Data1D'\
296                    and (len(self.data2.x) != len(self.data1.x) or
297                             not all(i == j for i, j in zip(self.data1.x, self.data2.x))):
298                logging.warning('Cannot compute 1D data of different lengths')
299                self.cbData1.setStyleSheet(BG_RED)
300                self.cbData2.setStyleSheet(BG_RED)
301                return False
302
303            elif self.data1.__class__.__name__ == 'Data2D' \
304                    and (len(self.data2.qx_data) != len(self.data1.qx_data) \
305                    or len(self.data2.qy_data) != len(self.data1.qy_data)
306                    or not all(i == j for i, j in
307                                     zip(self.data1.qx_data, self.data2.qx_data))
308                    or not all(i == j for i, j in
309                                zip(self.data1.qy_data, self.data2.qy_data))
310                         ):
311                self.cbData1.setStyleSheet(BG_RED)
312                self.cbData2.setStyleSheet(BG_RED)
313                logging.warning('Cannot compute 2D data of different lengths')
314                return False
315
316            else:
317                self.cbData1.setStyleSheet(BG_WHITE)
318                self.cbData2.setStyleSheet(BG_WHITE)
319                return True
320
321    def onCheckOutputName(self):
322        """ Check that name of output does not already exist """
323        name_to_check = str(self.txtOutputData.text())
324        self.txtOutputData.setStyleSheet(BG_WHITE)
325
326        if name_to_check is None or name_to_check == '':
327            self.txtOutputData.setStyleSheet(BG_RED)
328            logging.warning('No output name')
329            return False
330
331        elif name_to_check in self.list_data_items:
332            self.txtOutputData.setStyleSheet(BG_RED)
333            logging.warning('The Output data name already exists')
334            return False
335
336        else:
337            self.txtOutputData.setStyleSheet(BG_WHITE)
338            return True
339
340    # ########
341    # Modification of inputs
342    # ########
343    def _findId(self, name):
344        """ find id of name in list of filenames """
345        isinstance(name, str)
346
347        for key_id in list(self.filenames.keys()):
348            # data with title
349            if self.filenames[key_id].get_data().title:
350                input = self.filenames[key_id].get_data().title
351            # data without title
352            else:
353                input = str(key_id)
354            if name in input:
355                return key_id
356
357    def _extractData(self, key_id):
358        """ Extract data from file with id contained in list of filenames """
359        data_complete = self.filenames[key_id].get_data()
360        dimension = data_complete.__class__.__name__
361
362        if dimension in ('Data1D', 'Data2D'):
363            return copy.deepcopy(data_complete)
364
365        else:
366            logging.warning('Error with data format')
367            return
368
369    # ########
370    # PLOTS
371    # ########
372    def newPlot(self, graph, layout):
373        """ Create template for graphs with default '?' layout"""
374        assert isinstance(graph, QtWidgets.QGraphicsView)
375        assert isinstance(layout, QtWidgets.QHBoxLayout)
376
377        # clear layout
378        if layout.count() > 0:
379            item = layout.takeAt(0)
380            layout.removeItem(item)
381
382        layout.setContentsMargins(0, 0, 0, 0)
383        layout.addWidget(self.prepareSubgraphWithData("?"))
384
385        graph.setLayout(layout)
386
387    def updatePlot(self, graph, layout, data):
388        """ plot data in graph after clearing its layout """
389
390        assert isinstance(graph, QtWidgets.QGraphicsView)
391        assert isinstance(layout, QtWidgets.QHBoxLayout)
392
393        # clear layout
394        if layout.count() > 0:
395            item = layout.takeAt(0)
396            layout.removeItem(item)
397
398        layout.setContentsMargins(0, 0, 0, 0)
399
400        if isinstance(data, Data2D):
401            # plot 2D data
402            plotter2D = Plotter2DWidget(self, quickplot=True)
403            plotter2D.data = data
404            plotter2D.scale = 'linear'
405
406            plotter2D.ax.tick_params(axis='x', labelsize=8)
407            plotter2D.ax.tick_params(axis='y', labelsize=8)
408
409            # Draw zero axis lines.
410            plotter2D.ax.axhline(linewidth=1)
411            plotter2D.ax.axvline(linewidth=1)
412
413            graph.setLayout(layout)
414            layout.addWidget(plotter2D)
415            # remove x- and ylabels
416            plotter2D.y_label = ''
417            plotter2D.x_label = ''
418            plotter2D.plot(show_colorbar=False)
419            plotter2D.show()
420
421        elif isinstance(data, Data1D):
422            # plot 1D data
423            plotter = PlotterWidget(self, quickplot=True)
424            data.scale = 'linear'
425            plotter.data = data
426            plotter.showLegend = False
427            graph.setLayout(layout)
428            layout.addWidget(plotter)
429
430            plotter.ax.tick_params(axis='x', labelsize=8)
431            plotter.ax.tick_params(axis='y', labelsize=8)
432
433            plotter.plot(hide_error=True, marker='.')
434
435            plotter.show()
436
437        elif float(data) and self.cbData2.currentText() == 'Number':
438            # display value of coefficient (to be applied to Data1)
439            # in graphData2
440            layout.addWidget(self.prepareSubgraphWithData(data))
441
442            graph.setLayout(layout)
443
444    def prepareSubgraphWithData(self, data):
445        """ Create graphics view containing scene with string """
446        scene = QtWidgets.QGraphicsScene()
447        scene.addText(str(data))
448
449        subgraph = QtWidgets.QGraphicsView()
450        subgraph.setScene(scene)
451
452        return subgraph
Note: See TracBrowser for help on using the repository browser.