source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py @ 99372b6

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

Added a mini-button for easier Add New Fitpage action. From TRAC #1196

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