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

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 7969b9c was 4992ff2, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Initial, in-progress version. Not really working atm. SASVIEW-787

  • Property mode set to 100644
File size: 16.5 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
58        self.txtNumber.setValidator(QtGui.QDoubleValidator())
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        """
112        try:
113            location = GuiUtils.HELP_DIRECTORY_LOCATION + \
114                       "/user/sasgui/perspectives/calculator/data_operator_help.html"
115            self.manager._helpView.load(QtCore.QUrl(location))
116            self.manager._helpView.show()
117
118        except AttributeError:
119            # No manager defined - testing and standalone runs
120            pass
121
122    def onClose(self):
123        """ Close dialog """
124        self.onReset()
[1420066]125
126        self.cbData1.clear()
127        self.cbData1.addItems(['No Data Available'])
128        self.cbData2.clear()
129        self.cbData2.addItems(['No Data Available'])
[d5c5d3d]130        self.close()
131
[1420066]132
[d5c5d3d]133    def onCompute(self):
134        """ perform calculation """
[0c468bf]135        # set operator to be applied
136        operator = self.cbOperator.currentText()
137        # calculate and send data to DataExplorer
138        output = None
139        try:
140            data1 = self.data1
141            data2 = self.data2
[b3e8629]142            exec("output = data1 %s data2" % operator)
[0c468bf]143        except:
144            raise
145
146        self.output = output
147
148        # if outputname was unused, write output result to it
149        # and display plot
150        if self.onCheckOutputName():
151            # add outputname to self.filenames
152            self.list_data_items.append(str(self.txtOutputData.text()))
153            # send result to DataExplorer
154            self.onPrepareOutputData()
155            # plot result
156            self.updatePlot(self.graphOutput, self.layoutOutput, self.output)
[d5c5d3d]157
158    def onPrepareOutputData(self):
159        """ Prepare datasets to be added to DataExplorer and DataManager """
160        new_item = GuiUtils.createModelItemWithPlot(
[cee5c78]161            self.output,
[d5c5d3d]162            name=self.txtOutputData.text())
163
164        new_datalist_item = {str(self.txtOutputData.text()) + str(time.time()):
165                                 self.output}
166        self.communicator. \
167            updateModelFromDataOperationPanelSignal.emit(new_item, new_datalist_item)
168
169    def onSelectOperator(self):
[f0bb711]170        """ Change GUI when operator changed """
[d5c5d3d]171        self.lblOperatorApplied.setText(self.cbOperator.currentText())
172        self.newPlot(self.graphOutput, self.layoutOutput)
173
174    def onReset(self):
175        """
176        Reset Panel to its initial state (default values) keeping
177        the names of loaded data
178        """
179        self.txtNumber.setText('1.0')
180        self.txtOutputData.setText('MyNewDataName')
181
182        self.txtNumber.setEnabled(False)
183        self.cmdCompute.setEnabled(False)
184
185        self.cbData1.setCurrentIndex(0)
186        self.cbData2.setCurrentIndex(0)
187        self.cbOperator.setCurrentIndex(0)
188
189        self.output = None
190        self.data1 = None
191        self.data2 = None
[f0bb711]192        self.filenames = None
193        self.list_data_items = []
[d5c5d3d]194
195        self.data1OK = False
196        self.data2OK = False
197
198        # Empty graphs
199        self.newPlot(self.graphOutput, self.layoutOutput)
200        self.newPlot(self.graphData1, self.layoutData1)
201        self.newPlot(self.graphData2, self.layoutData2)
202
203    def onSelectData1(self):
204        """ Plot for selection of Data1 """
205        choice_data1 = str(self.cbData1.currentText())
206
207        wrong_choices = ['No Data Available', 'Select Data', '']
208
209        if choice_data1 in wrong_choices:
210            # check validity of choice: input = filename
211            self.newPlot(self.graphData1, self.layoutData1)
212            self.data1 = None
213            self.data1OK = False
[0c468bf]214            self.cmdCompute.setEnabled(False) # self.onCheckChosenData())
[d5c5d3d]215            return
216
217        else:
218            self.data1OK = True
219            # get Data1
[f0bb711]220            key_id1 = self._findId(choice_data1)
221            self.data1 = self._extractData(key_id1)
[d5c5d3d]222            # plot Data1
223            self.updatePlot(self.graphData1, self.layoutData1, self.data1)
224            # plot default for output graph
225            self.newPlot(self.graphOutput, self.layoutOutput)
[0c468bf]226            # Enable Compute button only if Data2 is defined and data compatible
227            self.cmdCompute.setEnabled(self.onCheckChosenData())
[f0bb711]228
[d5c5d3d]229    def onSelectData2(self):
230        """ Plot for selection of Data2 """
231        choice_data2 = str(self.cbData2.currentText())
232        wrong_choices = ['No Data Available', 'Select Data', '']
233
234        if choice_data2 in wrong_choices:
235            self.newPlot(self.graphData2, self.layoutData2)
236            self.txtNumber.setEnabled(False)
237            self.data2OK = False
[f0bb711]238            self.onCheckChosenData()
[0c468bf]239            self.cmdCompute.setEnabled(False)
[d5c5d3d]240            return
241
242        elif choice_data2 == 'Number':
243            self.data2OK = True
244            self.txtNumber.setEnabled(True)
245            self.data2 = float(self.txtNumber.text())
246
[0c468bf]247            # Enable Compute button only if Data1 defined and compatible data
248            self.cmdCompute.setEnabled(self.onCheckChosenData())
[d5c5d3d]249            # Display value of coefficient in graphData2
250            self.updatePlot(self.graphData2, self.layoutData2, self.data2)
251            # plot default for output graph
252            self.newPlot(self.graphOutput, self.layoutOutput)
[f0bb711]253            self.onCheckChosenData()
[d5c5d3d]254
255        else:
256            self.txtNumber.setEnabled(False)
[0c468bf]257            self.data2OK = True
[f0bb711]258            key_id2 = self._findId(choice_data2)
259            self.data2 = self._extractData(key_id2)
[0c468bf]260            self.cmdCompute.setEnabled(self.onCheckChosenData())
261
[d5c5d3d]262            # plot Data2
263            self.updatePlot(self.graphData2, self.layoutData2, self.data2)
264            # plot default for output graph
265            self.newPlot(self.graphOutput, self.layoutOutput)
266
267    def onInputCoefficient(self):
268        """ Check input of number when a coefficient is required
269        for operation """
270        if self.txtNumber.isModified():
271            input_to_check = str(self.txtNumber.text())
272
273            if input_to_check is None or input_to_check is '':
274                msg = 'DataOperation: Number requires a float number'
[7d9c83c]275                logging.warning(msg)
[7fb471d]276                self.txtNumber.setStyleSheet(BG_RED)
[d5c5d3d]277
278            elif float(self.txtNumber.text()) == 0.:
279                # should be check that 0 is not chosen
280                msg = 'DataOperation: Number requires a non zero number'
[7d9c83c]281                logging.warning(msg)
[7fb471d]282                self.txtNumber.setStyleSheet(BG_RED)
[d5c5d3d]283
284            else:
[7fb471d]285                self.txtNumber.setStyleSheet(BG_WHITE)
[d5c5d3d]286                self.data2 = float(self.txtNumber.text())
287                self.updatePlot(self.graphData2, self.layoutData2, self.data2)
288
289    def onCheckChosenData(self):
[0c468bf]290        """ check that data1 and data2 are compatible """
[d5c5d3d]291
[0c468bf]292        if not all([self.data1OK, self.data2OK]):
[d5c5d3d]293            return False
294        else:
[0c468bf]295            if self.cbData2.currentText() == 'Number':
[7fb471d]296                self.cbData1.setStyleSheet(BG_WHITE)
297                self.cbData2.setStyleSheet(BG_WHITE)
[0c468bf]298                return True
299
300            elif self.data1.__class__.__name__ != self.data2.__class__.__name__:
[7fb471d]301                self.cbData1.setStyleSheet(BG_RED)
302                self.cbData2.setStyleSheet(BG_RED)
[b3e8629]303                print(self.data1.__class__.__name__ != self.data2.__class__.__name__)
[7d9c83c]304                logging.warning('Cannot compute data of different dimensions')
[0c468bf]305                return False
306
307            elif self.data1.__class__.__name__ == 'Data1D'\
308                    and (len(self.data2.x) != len(self.data1.x) or
309                             not all(i == j for i, j in zip(self.data1.x, self.data2.x))):
[7d9c83c]310                logging.warning('Cannot compute 1D data of different lengths')
[7fb471d]311                self.cbData1.setStyleSheet(BG_RED)
312                self.cbData2.setStyleSheet(BG_RED)
[0c468bf]313                return False
314
315            elif self.data1.__class__.__name__ == 'Data2D' \
316                    and (len(self.data2.qx_data) != len(self.data1.qx_data) \
317                    or len(self.data2.qy_data) != len(self.data1.qy_data)
318                    or not all(i == j for i, j in
319                                     zip(self.data1.qx_data, self.data2.qx_data))
320                    or not all(i == j for i, j in
321                                zip(self.data1.qy_data, self.data2.qy_data))
322                         ):
[7fb471d]323                self.cbData1.setStyleSheet(BG_RED)
324                self.cbData2.setStyleSheet(BG_RED)
[7d9c83c]325                logging.warning('Cannot compute 2D data of different lengths')
[0c468bf]326                return False
327
328            else:
[7fb471d]329                self.cbData1.setStyleSheet(BG_WHITE)
330                self.cbData2.setStyleSheet(BG_WHITE)
[0c468bf]331                return True
[d5c5d3d]332
333    def onCheckOutputName(self):
334        """ Check that name of output does not already exist """
335        name_to_check = str(self.txtOutputData.text())
[7fb471d]336        self.txtOutputData.setStyleSheet(BG_WHITE)
[d5c5d3d]337
338        if name_to_check is None or name_to_check == '':
[7fb471d]339            self.txtOutputData.setStyleSheet(BG_RED)
[7d9c83c]340            logging.warning('No output name')
[d5c5d3d]341            return False
342
343        elif name_to_check in self.list_data_items:
[7fb471d]344            self.txtOutputData.setStyleSheet(BG_RED)
[7d9c83c]345            logging.warning('The Output data name already exists')
[d5c5d3d]346            return False
347
348        else:
[7fb471d]349            self.txtOutputData.setStyleSheet(BG_WHITE)
[d5c5d3d]350            return True
351
352    # ########
353    # Modification of inputs
354    # ########
355    def _findId(self, name):
356        """ find id of name in list of filenames """
[b3e8629]357        isinstance(name, str)
[d5c5d3d]358
[b3e8629]359        for key_id in list(self.filenames.keys()):
[d5c5d3d]360            # data with title
[f0bb711]361            if self.filenames[key_id].get_data().title:
362                input = self.filenames[key_id].get_data().title
[d5c5d3d]363            # data without title
364            else:
[f0bb711]365                input = str(key_id)
[d5c5d3d]366            if name in input:
[f0bb711]367                return key_id
[d5c5d3d]368
[f0bb711]369    def _extractData(self, key_id):
[d5c5d3d]370        """ Extract data from file with id contained in list of filenames """
[f0bb711]371        data_complete = self.filenames[key_id].get_data()
[d5c5d3d]372        dimension = data_complete.__class__.__name__
373
[f0bb711]374        if dimension in ('Data1D', 'Data2D'):
375            return copy.deepcopy(data_complete)
[d5c5d3d]376
377        else:
[7d9c83c]378            logging.warning('Error with data format')
[d5c5d3d]379            return
380
381    # ########
382    # PLOTS
383    # ########
384    def newPlot(self, graph, layout):
385        """ Create template for graphs with default '?' layout"""
[4992ff2]386        assert isinstance(graph, QtWidgets.QGraphicsView)
387        assert isinstance(layout, QtWidgets.QHBoxLayout)
[d5c5d3d]388
389        # clear layout
390        if layout.count() > 0:
391            item = layout.takeAt(0)
392            layout.removeItem(item)
393
394        layout.setContentsMargins(0, 0, 0, 0)
[f0bb711]395        layout.addWidget(self.prepareSubgraphWithData("?"))
[d5c5d3d]396
397        graph.setLayout(layout)
398
399    def updatePlot(self, graph, layout, data):
400        """ plot data in graph after clearing its layout """
401
[4992ff2]402        assert isinstance(graph, QtWidgets.QGraphicsView)
403        assert isinstance(layout, QtWidgets.QHBoxLayout)
[d5c5d3d]404
405        # clear layout
406        if layout.count() > 0:
407            item = layout.takeAt(0)
408            layout.removeItem(item)
409
410        layout.setContentsMargins(0, 0, 0, 0)
411
412        if isinstance(data, Data2D):
413            # plot 2D data
414            plotter2D = Plotter2DWidget(self, quickplot=True)
415            plotter2D.data = data
416            plotter2D.scale = 'linear'
417
418            plotter2D.ax.tick_params(axis='x', labelsize=8)
419            plotter2D.ax.tick_params(axis='y', labelsize=8)
420
421            # Draw zero axis lines.
422            plotter2D.ax.axhline(linewidth=1)
423            plotter2D.ax.axvline(linewidth=1)
424
425            graph.setLayout(layout)
426            layout.addWidget(plotter2D)
427            # remove x- and ylabels
428            plotter2D.y_label = ''
429            plotter2D.x_label = ''
430            plotter2D.plot(show_colorbar=False)
431            plotter2D.show()
432
433        elif isinstance(data, Data1D):
434            # plot 1D data
435            plotter = PlotterWidget(self, quickplot=True)
436            plotter.data = data
437
438            graph.setLayout(layout)
439            layout.addWidget(plotter)
440
441            plotter.ax.tick_params(axis='x', labelsize=8)
442            plotter.ax.tick_params(axis='y', labelsize=8)
443
[f0bb711]444            plotter.plot(hide_error=True, marker='.')
445            # plotter.legend = None
[d5c5d3d]446
447            plotter.show()
448
449        elif float(data) and self.cbData2.currentText() == 'Number':
450            # display value of coefficient (to be applied to Data1)
451            # in graphData2
[f0bb711]452            layout.addWidget(self.prepareSubgraphWithData(data))
[d5c5d3d]453
[f0bb711]454            graph.setLayout(layout)
[d5c5d3d]455
[f0bb711]456    def prepareSubgraphWithData(self, data):
457        """ Create graphics view containing scene with string """
[4992ff2]458        scene = QtWidgets.QGraphicsScene()
[f0bb711]459        scene.addText(str(data))
[d5c5d3d]460
[4992ff2]461        subgraph = QtWidgets.QGraphicsView()
[f0bb711]462        subgraph.setScene(scene)
463
464        return subgraph
Note: See TracBrowser for help on using the repository browser.