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
Line 
1import numpy
2import copy
3
4from PyQt5 import QtCore
5from PyQt5 import QtGui
6from PyQt5 import QtWidgets
7
8from bumps import options
9from bumps import fitters
10
11import sas.qtgui.Utilities.LocalConfig as LocalConfig
12import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
13import sas.qtgui.Utilities.GuiUtils as GuiUtils
14
15from sas.qtgui.Perspectives.Fitting.FittingWidget import FittingWidget
16from sas.qtgui.Perspectives.Fitting.ConstraintWidget import ConstraintWidget
17from sas.qtgui.Perspectives.Fitting.FittingOptions import FittingOptions
18from sas.qtgui.Perspectives.Fitting.GPUOptions import GPUOptions
19
20class FittingWindow(QtWidgets.QTabWidget):
21    """
22    """
23    tabsModifiedSignal = QtCore.pyqtSignal()
24    fittingStartedSignal = QtCore.pyqtSignal(list)
25    fittingStoppedSignal = QtCore.pyqtSignal(list)
26
27    name = "Fitting" # For displaying in the combo box in DataExplorer
28    def __init__(self, parent=None, data=None):
29
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
39        self.maxIndex = 1
40
41        # The default optimizer
42        self.optimizer = 'Levenberg-Marquardt'
43
44        # Dataset index -> Fitting tab mapping
45        self.dataToFitTab = {}
46
47        # The tabs need to be closeable
48        self.setTabsClosable(True)
49
50        # The tabs need to be movabe
51        self.setMovable(True)
52
53        self.communicate = self.parent.communicator()
54
55        # Initialize the first tab
56        self.addFit(None)
57
58        # Deal with signals
59        self.tabCloseRequested.connect(self.tabCloses)
60        self.communicate.dataDeletedSignal.connect(self.dataDeleted)
61        self.fittingStartedSignal.connect(self.onFittingStarted)
62        self.fittingStoppedSignal.connect(self.onFittingStopped)
63
64        self.communicate.copyFitParamsSignal.connect(self.onParamCopy)
65        self.communicate.pasteFitParamsSignal.connect(self.onParamPaste)
66        self.communicate.copyExcelFitParamsSignal.connect(self.onExcelCopy)
67        self.communicate.copyLatexFitParamsSignal.connect(self.onLatexCopy)
68
69
70        # Perspective window not allowed to close by default
71        self._allow_close = False
72
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
81        # GPU Options
82        self.gpu_options_widget = GPUOptions(self)
83
84        self.updateWindowTitle()
85
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
92    def updateWindowTitle(self):
93        """
94        Update the window title with the current optimizer name
95        """
96        self.optimizer = self.fit_options.selected_name
97        self.setWindowTitle('Fit panel - Active Fitting Optimizer: %s' % self.optimizer)
98
99
100    def setClosable(self, value=True):
101        """
102        Allow outsiders to close this widget
103        """
104        assert isinstance(value, bool)
105
106        self._allow_close = value
107
108    def onParamCopy(self):
109        self.currentTab.onCopyToClipboard("")
110
111    def onParamPaste(self):
112        self.currentTab.onParameterPaste()
113
114    def onExcelCopy(self):
115        self.currentTab.onCopyToClipboard("Excel")
116
117    def onLatexCopy(self):
118        self.currentTab.onCopyToClipboard("Latex")
119
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)
126            if 'data_id' not in tab_data: continue
127            id = tab_data['data_id'][0]
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]
139        return params
140
141    def serializeCurrentFitpage(self):
142        # serialize current(active) fitpage
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()
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        """
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
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
178    def closeEvent(self, event):
179        """
180        Overwrite QDialog close method to allow for custom widget close
181        """
182        # Invoke fit page events
183        if self._allow_close:
184            # reset the closability flag
185            self.setClosable(value=False)
186            # Tell the MdiArea to close the container
187            self.parentWidget().close()
188            event.accept()
189        else:
190            # Maybe we should just minimize
191            self.setWindowState(QtCore.Qt.WindowMinimized)
192            event.ignore()
193
194    def addFit(self, data, is_batch=False):
195        """
196        Add a new tab for passed data
197        """
198        tab     = FittingWidget(parent=self.parent, data=data, tab_id=self.maxIndex)
199        tab.is_batch_fitting = is_batch
200
201        # Add this tab to the object library so it can be retrieved by scripting/jupyter
202        tab_name = self.getTabName(is_batch=is_batch)
203        ObjectLibrary.addObject(tab_name, tab)
204        self.tabs.append(tab)
205        if data:
206            self.updateFitDict(data, tab_name)
207        self.maxIndex += 1
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
213        self.setCurrentWidget(tab);
214        # Notify listeners
215        self.tabsModifiedSignal.emit()
216
217    def addConstraintTab(self):
218        """
219        Add a new C&S fitting tab
220        """
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)
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)
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)
237
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        """
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)
245        else:
246            self.dataToFitTab[item_key_str] = [tab_name]
247
248    def getTabName(self, is_batch=False):
249        """
250        Get the new tab name, based on the number of fitting tabs so far
251        """
252        page_name = "BatchPage" if is_batch else "FitPage"
253        page_name = page_name + str(self.maxIndex)
254        return page_name
255
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
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
269        if index in self.tabs and not self.tabs[index].data:
270            return
271        # Add a new, empy tab
272        self.addFit(None)
273        # Remove the previous last tab
274        self.tabCloses(index)
275
276    def tabCloses(self, index):
277        """
278        Update local bookkeeping on tab close
279        """
280        #assert len(self.tabs) >= index
281        # don't remove the last tab
282        if len(self.tabs) <= 1:
283            self.resetTab(index)
284            return
285        try:
286            ObjectLibrary.deleteObjectByRef(self.tabs[index])
287            self.removeTab(index)
288            del self.tabs[index]
289            self.tabsModifiedSignal.emit()
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        """
298        for tab_index in range(len(self.tabs)):
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:
310            index_to_delete_str = str(index_to_delete)
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)
317
318    def allowBatch(self):
319        """
320        Tell the caller that we accept multiple data instances
321        """
322        return True
323
324    def isSerializable(self):
325        """
326        Tell the caller that this perspective writes its state
327        """
328        return True
329
330    def setData(self, data_item=None, is_batch=False):
331        """
332        Assign new dataset to the fitting instance
333        Obtain a QStandardItem object and dissect it to get Data1D/2D
334        Pass it over to the calculator
335        """
336        assert data_item is not None
337
338        if not isinstance(data_item, list):
339            msg = "Incorrect type passed to the Fitting Perspective"
340            raise AttributeError(msg)
341
342        if not isinstance(data_item[0], QtGui.QStandardItem):
343            msg = "Incorrect type passed to the Fitting Perspective"
344            raise AttributeError(msg)
345
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
350
351        items = [data_item] if is_batch else data_item
352        for data in items:
353            # Find the first unassigned tab.
354            # If none, open a new tab.
355            available_tabs = [tab.acceptsData() for tab in self.tabs]
356
357            if numpy.any(available_tabs):
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)
362            else:
363                self.addFit(data, is_batch=is_batch)
364
365    def onFittingOptionsChange(self, fit_engine):
366        """
367        React to the fitting algorithm change by modifying window title
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
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]):
388                tab_object.disableInteractiveElements()
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]):
405                tab_object.enableInteractiveElements()
406
407        pass
408
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
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.