source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py @ 17e2d502

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 17e2d502 was 17e2d502, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Batch page serialization/deserialization

  • Property mode set to 100644
File size: 13.4 KB
RevLine 
[6f7f652]1import numpy
[0efe791]2
[4992ff2]3from PyQt5 import QtCore
4from PyQt5 import QtGui
5from PyQt5 import QtWidgets
[4c7dd9f]6
[2d0e0c1]7from bumps import options
8from bumps import fitters
9
[3b3b40b]10import sas.qtgui.Utilities.LocalConfig as LocalConfig
[61a92d4]11import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
[5236449]12
[1bc27f1]13from sas.qtgui.Perspectives.Fitting.FittingWidget import FittingWidget
[676f137]14from sas.qtgui.Perspectives.Fitting.ConstraintWidget import ConstraintWidget
[2d0e0c1]15from sas.qtgui.Perspectives.Fitting.FittingOptions import FittingOptions
[06ce180]16from sas.qtgui.Perspectives.Fitting.GPUOptions import GPUOptions
[8bdb6f5]17
[4992ff2]18class FittingWindow(QtWidgets.QTabWidget):
[0efe791]19    """
20    """
[be8f4b0]21    tabsModifiedSignal = QtCore.pyqtSignal()
[14ec91c5]22    fittingStartedSignal = QtCore.pyqtSignal(list)
23    fittingStoppedSignal = QtCore.pyqtSignal(list)
24
[60af928]25    name = "Fitting" # For displaying in the combo box in DataExplorer
[811bec1]26    def __init__(self, parent=None, data=None):
[4992ff2]27
[f46f6dc]28        super(FittingWindow, self).__init__()
29
30        self.parent = parent
31        self._data = data
32
33        # List of active fits
34        self.tabs = []
35
36        # Max index for adding new, non-clashing tab names
[6b75aee]37        self.maxIndex = 1
[f46f6dc]38
[57be490]39        ## Index of the current tab
40        #self.currentTab = 0
[f46f6dc]41
[38eb433]42        # The default optimizer
[1bc27f1]43        self.optimizer = 'Levenberg-Marquardt'
[f46f6dc]44
[14ec91c5]45        # Dataset index -> Fitting tab mapping
[38eb433]46        self.dataToFitTab = {}
47
[f46f6dc]48        # The tabs need to be closeable
49        self.setTabsClosable(True)
[60af928]50
[14ec91c5]51        # The tabs need to be movabe
52        self.setMovable(True)
53
[811bec1]54        self.communicate = self.parent.communicator()
55
[60af928]56        # Initialize the first tab
[f46f6dc]57        self.addFit(None)
58
59        # Deal with signals
60        self.tabCloseRequested.connect(self.tabCloses)
[38eb433]61        self.communicate.dataDeletedSignal.connect(self.dataDeleted)
[14ec91c5]62        self.fittingStartedSignal.connect(self.onFittingStarted)
63        self.fittingStoppedSignal.connect(self.onFittingStopped)
[f46f6dc]64
[085409e3]65        self.communicate.copyFitParamsSignal.connect(self.onParamCopy)
66        self.communicate.pasteFitParamsSignal.connect(self.onParamPaste)
[20f4857]67        self.communicate.copyExcelFitParamsSignal.connect(self.onExcelCopy)
68        self.communicate.copyLatexFitParamsSignal.connect(self.onLatexCopy)
69
[085409e3]70
[b1e36a3]71        # Perspective window not allowed to close by default
72        self._allow_close = False
73
[2d0e0c1]74        # Fit options - uniform for all tabs
75        self.fit_options = options.FIT_CONFIG
76        self.fit_options_widget = FittingOptions(self, config=self.fit_options)
77        self.fit_options.selected_id = fitters.LevenbergMarquardtFit.id
78
79        # Listen to GUI Manager signal updating fit options
80        self.fit_options_widget.fit_option_changed.connect(self.onFittingOptionsChange)
81
[06ce180]82        # GPU Options
[14fa542]83        self.gpu_options_widget = GPUOptions(self)
[06ce180]84
[2d0e0c1]85        self.updateWindowTitle()
86
87    def updateWindowTitle(self):
88        """
89        Update the window title with the current optimizer name
90        """
91        self.optimizer = self.fit_options.selected_name
[f46f6dc]92        self.setWindowTitle('Fit panel - Active Fitting Optimizer: %s' % self.optimizer)
[60af928]93
[2d0e0c1]94
[b1e36a3]95    def setClosable(self, value=True):
96        """
[14ec91c5]97        Allow outsiders to close this widget
[b1e36a3]98        """
99        assert isinstance(value, bool)
100
101        self._allow_close = value
102
[085409e3]103    def onParamCopy(self):
[d2007a8]104        self.currentTab.onCopyToClipboard("")
[085409e3]105
106    def onParamPaste(self):
107        self.currentTab.onParameterPaste()
108
[20f4857]109    def onExcelCopy(self):
[d2007a8]110        self.currentTab.onCopyToClipboard("Excel")
[20f4857]111
112    def onLatexCopy(self):
[d2007a8]113        self.currentTab.onCopyToClipboard("Latex")
[20f4857]114
[a3c59503]115    def serializeAllFitpage(self):
116        # serialize all active fitpages and return
117        # a dictionary: {data_id: fitpage_state}
118        params = {}
119        for i, tab in enumerate(self.tabs):
120            tab_data = self.getSerializedFitpage(tab)
121            if tab.tab_id is None: continue
[17e2d502]122            if 'data_id' not in tab_data: continue
[a3c59503]123            id = tab_data['data_id'][0]
[17e2d502]124            if isinstance(id, list):
125                for i in id:
126                    if i in params:
127                        params[i].append(tab_data)
128                    else:
129                        params[i] = [tab_data]
130            else:
131                if id in params:
132                    params[id].append(tab_data)
133                else:
134                    params[id] = [tab_data]
[a3c59503]135        return params
136
137    def serializeCurrentFitpage(self):
[2eeda93]138        # serialize current(active) fitpage
[a3c59503]139        return self.getSerializedFitpage(self.currentTab)
140
141    def getSerializedFitpage(self, tab):
142        """
143        get serialize requested fit tab
144        """
145        fitpage_state = tab.getFitPage()
146        fitpage_state += tab.getFitModel()
[2eeda93]147        # put the text into dictionary
148        line_dict = {}
149        for line in fitpage_state:
150            #content = line.split(',')
151            if len(line) > 1:
152                line_dict[line[0]] = line[1:]
153        return line_dict
154
155    def currentTabDataId(self):
156        """
157        Returns the data ID of the current tab
158        """
159        tab_id = None
160        if self.currentTab.data:
161            tab_id = self.currentTab.data.id
162        return tab_id
163
164    def updateFromParameters(self, parameters):
165        """
166        Pass the update parameters to the current fit page
167        """
168        self.currentTab.createPageForParameters(parameters)
169
[b1e36a3]170    def closeEvent(self, event):
171        """
172        Overwrite QDialog close method to allow for custom widget close
173        """
[2add354]174        # Invoke fit page events
[b1e36a3]175        if self._allow_close:
176            # reset the closability flag
177            self.setClosable(value=False)
[7c487846]178            # Tell the MdiArea to close the container
179            self.parentWidget().close()
[b1e36a3]180            event.accept()
181        else:
182            # Maybe we should just minimize
183            self.setWindowState(QtCore.Qt.WindowMinimized)
[2add354]184            event.ignore()
[b1e36a3]185
[ee18d33]186    def addFit(self, data, is_batch=False):
[60af928]187        """
188        Add a new tab for passed data
189        """
[10fee37]190        tab     = FittingWidget(parent=self.parent, data=data, tab_id=self.maxIndex)
[ee18d33]191        tab.is_batch_fitting = is_batch
[3b3b40b]192
[61a92d4]193        # Add this tab to the object library so it can be retrieved by scripting/jupyter
[676f137]194        tab_name = self.getTabName(is_batch=is_batch)
[38eb433]195        ObjectLibrary.addObject(tab_name, tab)
[f46f6dc]196        self.tabs.append(tab)
[38eb433]197        if data:
198            self.updateFitDict(data, tab_name)
[f46f6dc]199        self.maxIndex += 1
[3b3b40b]200        icon = QtGui.QIcon()
201        if is_batch:
202            icon.addPixmap(QtGui.QPixmap("src/sas/qtgui/images/icons/layers.svg"))
203        self.addTab(tab, icon, tab_name)
204        # Show the new tab
[02e7d3a]205        self.setCurrentWidget(tab);
[3b3b40b]206        # Notify listeners
[be8f4b0]207        self.tabsModifiedSignal.emit()
[38eb433]208
[676f137]209    def addConstraintTab(self):
210        """
211        Add a new C&S fitting tab
212        """
[14ec91c5]213        tabs = [isinstance(tab, ConstraintWidget) for tab in self.tabs]
214        if any(tabs):
215            # We already have a C&S tab: show it
216            self.setCurrentIndex(tabs.index(True))
217            return
218        tab     = ConstraintWidget(parent=self)
[676f137]219        # Add this tab to the object library so it can be retrieved by scripting/jupyter
220        tab_name = self.getCSTabName() # TODO update the tab name scheme
221        ObjectLibrary.addObject(tab_name, tab)
222        self.tabs.append(tab)
[3b3b40b]223        icon = QtGui.QIcon()
224        icon.addPixmap(QtGui.QPixmap("src/sas/qtgui/images/icons/link.svg"))
225        self.addTab(tab, icon, tab_name)
226
227        # This will be the last tab, so set the index accordingly
228        self.setCurrentIndex(self.count()-1)
[676f137]229
[38eb433]230    def updateFitDict(self, item_key, tab_name):
231        """
232        Create a list if none exists and append if there's already a list
233        """
[b3e8629]234        item_key_str = str(item_key)
235        if item_key_str in list(self.dataToFitTab.keys()):
236            self.dataToFitTab[item_key_str].append(tab_name)
[38eb433]237        else:
[b3e8629]238            self.dataToFitTab[item_key_str] = [tab_name]
[f46f6dc]239
[676f137]240    def getTabName(self, is_batch=False):
[f46f6dc]241        """
242        Get the new tab name, based on the number of fitting tabs so far
243        """
[38eb433]244        page_name = "BatchPage" if is_batch else "FitPage"
245        page_name = page_name + str(self.maxIndex)
[f46f6dc]246        return page_name
247
[676f137]248    def getCSTabName(self):
249        """
250        Get the new tab name, based on the number of fitting tabs so far
251        """
252        page_name = "Const. & Simul. Fit"
253        return page_name
254
[38eb433]255    def resetTab(self, index):
256        """
257        Adds a new tab and removes the last tab
258        as a way of resetting the fit tabs
259        """
260        # If data on tab empty - do nothing
[377ade1]261        if index in self.tabs and not self.tabs[index].data:
[38eb433]262            return
263        # Add a new, empy tab
264        self.addFit(None)
265        # Remove the previous last tab
266        self.tabCloses(index)
267
[f46f6dc]268    def tabCloses(self, index):
269        """
270        Update local bookkeeping on tab close
271        """
[38eb433]272        #assert len(self.tabs) >= index
[f46f6dc]273        # don't remove the last tab
274        if len(self.tabs) <= 1:
[38eb433]275            self.resetTab(index)
[f46f6dc]276            return
[38eb433]277        try:
278            ObjectLibrary.deleteObjectByRef(self.tabs[index])
279            self.removeTab(index)
280            del self.tabs[index]
[be8f4b0]281            self.tabsModifiedSignal.emit()
[38eb433]282        except IndexError:
283            # The tab might have already been deleted previously
284            pass
285
286    def closeTabByName(self, tab_name):
287        """
288        Given name of the fitting tab - close it
289        """
[b3e8629]290        for tab_index in range(len(self.tabs)):
[38eb433]291            if self.tabText(tab_index) == tab_name:
292                self.tabCloses(tab_index)
293        pass # debug hook
294
295    def dataDeleted(self, index_list):
296        """
297        Delete fit tabs referencing given data
298        """
299        if not index_list or not self.dataToFitTab:
300            return
301        for index_to_delete in index_list:
[b3e8629]302            index_to_delete_str = str(index_to_delete)
303            if index_to_delete_str in list(self.dataToFitTab.keys()):
304                for tab_name in self.dataToFitTab[index_to_delete_str]:
[38eb433]305                    # delete tab #index after corresponding data got removed
306                    self.closeTabByName(tab_name)
[b3e8629]307                self.dataToFitTab.pop(index_to_delete_str)
[38eb433]308
[f46f6dc]309    def allowBatch(self):
310        """
311        Tell the caller that we accept multiple data instances
312        """
313        return True
314
[2eeda93]315    def isSerializable(self):
316        """
317        Tell the caller that this perspective writes its state
318        """
319        return True
320
[ee18d33]321    def setData(self, data_item=None, is_batch=False):
[f46f6dc]322        """
323        Assign new dataset to the fitting instance
[5236449]324        Obtain a QStandardItem object and dissect it to get Data1D/2D
325        Pass it over to the calculator
[f46f6dc]326        """
[cbcdd2c]327        assert data_item is not None
[68c96d3]328
[5236449]329        if not isinstance(data_item, list):
330            msg = "Incorrect type passed to the Fitting Perspective"
[b3e8629]331            raise AttributeError(msg)
[5236449]332
333        if not isinstance(data_item[0], QtGui.QStandardItem):
334            msg = "Incorrect type passed to the Fitting Perspective"
[b3e8629]335            raise AttributeError(msg)
[5236449]336
[17968c3]337        if is_batch:
338            # Just create a new fit tab. No empty batchFit tabs
339            self.addFit(data_item, is_batch=is_batch)
340            return
[ee18d33]341
[17968c3]342        items = [data_item] if is_batch else data_item
[ee18d33]343        for data in items:
[454670d]344            # Find the first unassigned tab.
345            # If none, open a new tab.
[17968c3]346            available_tabs = [tab.acceptsData() for tab in self.tabs]
[454670d]347
348            if numpy.any(available_tabs):
[38eb433]349                first_good_tab = available_tabs.index(True)
350                self.tabs[first_good_tab].data = data
351                tab_name = str(self.tabText(first_good_tab))
352                self.updateFitDict(data, tab_name)
[454670d]353            else:
[ee18d33]354                self.addFit(data, is_batch=is_batch)
[2d0e0c1]355
[b0c5e8c]356    def onFittingOptionsChange(self, fit_engine):
[2d0e0c1]357        """
[b0c5e8c]358        React to the fitting algorithm change by modifying window title
[2d0e0c1]359        """
360        fitter = [f.id for f in options.FITTERS if f.name == str(fit_engine)][0]
361        # set the optimizer
362        self.fit_options.selected_id = str(fitter)
363        # Update the title
364        self.updateWindowTitle()
365
[14ec91c5]366    def onFittingStarted(self, tabs_for_fitting=None):
367        """
368        Notify tabs listed in tabs_for_fitting
369        that the fitting thread started
370        """
371        assert(isinstance(tabs_for_fitting, list))
372        assert(len(tabs_for_fitting)>0)
373
374        for tab_object in self.tabs:
375            if not isinstance(tab_object, FittingWidget):
376                continue
377            page_name = "Page%s"%tab_object.tab_id
378            if any([page_name in tab for tab in tabs_for_fitting]):
[9d130f3]379                tab_object.disableInteractiveElements()
[14ec91c5]380
381        pass
382
383    def onFittingStopped(self, tabs_for_fitting=None):
384        """
385        Notify tabs listed in tabs_for_fitting
386        that the fitting thread stopped
387        """
388        assert(isinstance(tabs_for_fitting, list))
389        assert(len(tabs_for_fitting)>0)
390
391        for tab_object in self.tabs:
392            if not isinstance(tab_object, FittingWidget):
393                continue
394            page_name = "Page%s"%tab_object.tab_id
395            if any([page_name in tab for tab in tabs_for_fitting]):
[9d130f3]396                tab_object.enableInteractiveElements()
[14ec91c5]397
[2d0e0c1]398        pass
[57be490]399
[345b3b3]400    def getCurrentStateAsXml(self):
401        """
402        Returns an XML version of the current state
403        """
404        state = {}
405        for tab in self.tabs:
406            pass
407        return state
408
[57be490]409    @property
410    def currentTab(self):
411        """
412        Returns the tab widget currently shown
413        """
414        return self.currentWidget()
415
Note: See TracBrowser for help on using the repository browser.