source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py @ 14e1ff0

ESS_GUIESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_opencl
Last change on this file since 14e1ff0 was 14e1ff0, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 17 months ago

Allow for C&S fitting cancellation. SASVIEW-1280

  • 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.setToolTip("Add a new Fit Page")
91        self.plusButton.clicked.connect(lambda: self.addFit(None))
92
93    def updateWindowTitle(self):
94        """
95        Update the window title with the current optimizer name
96        """
97        self.optimizer = self.fit_options.selected_name
98        self.setWindowTitle('Fit panel - Active Fitting Optimizer: %s' % self.optimizer)
99
100
101    def setClosable(self, value=True):
102        """
103        Allow outsiders to close this widget
104        """
105        assert isinstance(value, bool)
106
107        self._allow_close = value
108
109    def onParamCopy(self):
110        self.currentTab.onCopyToClipboard("")
111
112    def onParamPaste(self):
113        self.currentTab.onParameterPaste()
114
115    def onExcelCopy(self):
116        self.currentTab.onCopyToClipboard("Excel")
117
118    def onLatexCopy(self):
119        self.currentTab.onCopyToClipboard("Latex")
120
121    def serializeAllFitpage(self):
122        # serialize all active fitpages and return
123        # a dictionary: {data_id: fitpage_state}
124        params = {}
125        for i, tab in enumerate(self.tabs):
126            tab_data = self.getSerializedFitpage(tab)
127            if 'data_id' not in tab_data: continue
128            id = tab_data['data_id'][0]
129            if isinstance(id, list):
130                for i in id:
131                    if i in params:
132                        params[i].append(tab_data)
133                    else:
134                        params[i] = [tab_data]
135            else:
136                if id in params:
137                    params[id].append(tab_data)
138                else:
139                    params[id] = [tab_data]
140        return params
141
142    def serializeCurrentFitpage(self):
143        # serialize current(active) fitpage
144        return self.getSerializedFitpage(self.currentTab)
145
146    def getSerializedFitpage(self, tab):
147        """
148        get serialize requested fit tab
149        """
150        fitpage_state = tab.getFitPage()
151        fitpage_state += tab.getFitModel()
152        # put the text into dictionary
153        line_dict = {}
154        for line in fitpage_state:
155            #content = line.split(',')
156            if len(line) > 1:
157                line_dict[line[0]] = line[1:]
158        return line_dict
159
160    def currentTabDataId(self):
161        """
162        Returns the data ID of the current tab
163        """
164        tab_id = []
165        if not self.currentTab.data:
166            return tab_id
167        for item in self.currentTab.all_data:
168            data = GuiUtils.dataFromItem(item)
169            tab_id.append(data.id)
170
171        return tab_id
172
173    def updateFromParameters(self, parameters):
174        """
175        Pass the update parameters to the current fit page
176        """
177        self.currentTab.createPageForParameters(parameters)
178
179    def closeEvent(self, event):
180        """
181        Overwrite QDialog close method to allow for custom widget close
182        """
183        # Invoke fit page events
184        if self._allow_close:
185            # reset the closability flag
186            self.setClosable(value=False)
187            # Tell the MdiArea to close the container
188            self.parentWidget().close()
189            event.accept()
190        else:
191            # Maybe we should just minimize
192            self.setWindowState(QtCore.Qt.WindowMinimized)
193            event.ignore()
194
195    def addFit(self, data, is_batch=False):
196        """
197        Add a new tab for passed data
198        """
199        tab     = FittingWidget(parent=self.parent, data=data, tab_id=self.maxIndex)
200        tab.is_batch_fitting = is_batch
201
202        # Add this tab to the object library so it can be retrieved by scripting/jupyter
203        tab_name = self.getTabName(is_batch=is_batch)
204        ObjectLibrary.addObject(tab_name, tab)
205        self.tabs.append(tab)
206        if data:
207            self.updateFitDict(data, tab_name)
208        self.maxIndex += 1
209        icon = QtGui.QIcon()
210        if is_batch:
211            icon.addPixmap(QtGui.QPixmap("src/sas/qtgui/images/icons/layers.svg"))
212        self.addTab(tab, icon, tab_name)
213        # Show the new tab
214        self.setCurrentWidget(tab);
215        # Notify listeners
216        self.tabsModifiedSignal.emit()
217
218    def addConstraintTab(self):
219        """
220        Add a new C&S fitting tab
221        """
222        tabs = [isinstance(tab, ConstraintWidget) for tab in self.tabs]
223        if any(tabs):
224            # We already have a C&S tab: show it
225            self.setCurrentIndex(tabs.index(True))
226            return
227        tab     = ConstraintWidget(parent=self)
228        # Add this tab to the object library so it can be retrieved by scripting/jupyter
229        tab_name = self.getCSTabName() # TODO update the tab name scheme
230        ObjectLibrary.addObject(tab_name, tab)
231        self.tabs.append(tab)
232        icon = QtGui.QIcon()
233        icon.addPixmap(QtGui.QPixmap("src/sas/qtgui/images/icons/link.svg"))
234        self.addTab(tab, icon, tab_name)
235
236        # This will be the last tab, so set the index accordingly
237        self.setCurrentIndex(self.count()-1)
238
239    def updateFitDict(self, item_key, tab_name):
240        """
241        Create a list if none exists and append if there's already a list
242        """
243        item_key_str = str(item_key)
244        if item_key_str in list(self.dataToFitTab.keys()):
245            self.dataToFitTab[item_key_str].append(tab_name)
246        else:
247            self.dataToFitTab[item_key_str] = [tab_name]
248
249    def getTabName(self, is_batch=False):
250        """
251        Get the new tab name, based on the number of fitting tabs so far
252        """
253        page_name = "BatchPage" if is_batch else "FitPage"
254        page_name = page_name + str(self.maxIndex)
255        return page_name
256
257    def getCSTabName(self):
258        """
259        Get the new tab name, based on the number of fitting tabs so far
260        """
261        page_name = "Const. & Simul. Fit"
262        return page_name
263
264    def resetTab(self, index):
265        """
266        Adds a new tab and removes the last tab
267        as a way of resetting the fit tabs
268        """
269        # If data on tab empty - do nothing
270        if index in self.tabs and not self.tabs[index].data:
271            return
272        # Add a new, empy tab
273        self.addFit(None)
274        # Remove the previous last tab
275        self.tabCloses(index)
276
277    def tabCloses(self, index):
278        """
279        Update local bookkeeping on tab close
280        """
281        #assert len(self.tabs) >= index
282        # don't remove the last tab
283        if len(self.tabs) <= 1:
284            self.resetTab(index)
285            return
286        try:
287            ObjectLibrary.deleteObjectByRef(self.tabs[index])
288            self.removeTab(index)
289            del self.tabs[index]
290            self.tabsModifiedSignal.emit()
291        except IndexError:
292            # The tab might have already been deleted previously
293            pass
294
295    def closeTabByName(self, tab_name):
296        """
297        Given name of the fitting tab - close it
298        """
299        for tab_index in range(len(self.tabs)):
300            if self.tabText(tab_index) == tab_name:
301                self.tabCloses(tab_index)
302        pass # debug hook
303
304    def dataDeleted(self, index_list):
305        """
306        Delete fit tabs referencing given data
307        """
308        if not index_list or not self.dataToFitTab:
309            return
310        for index_to_delete in index_list:
311            index_to_delete_str = str(index_to_delete)
312            orig_dict = copy.deepcopy(self.dataToFitTab)
313            for tab_key in orig_dict.keys():
314                if index_to_delete_str in tab_key:
315                    for tab_name in orig_dict[tab_key]:
316                        self.closeTabByName(tab_name)
317                    self.dataToFitTab.pop(tab_key)
318
319    def allowBatch(self):
320        """
321        Tell the caller that we accept multiple data instances
322        """
323        return True
324
325    def isSerializable(self):
326        """
327        Tell the caller that this perspective writes its state
328        """
329        return True
330
331    def setData(self, data_item=None, is_batch=False):
332        """
333        Assign new dataset to the fitting instance
334        Obtain a QStandardItem object and dissect it to get Data1D/2D
335        Pass it over to the calculator
336        """
337        assert data_item is not None
338
339        if not isinstance(data_item, list):
340            msg = "Incorrect type passed to the Fitting Perspective"
341            raise AttributeError(msg)
342
343        if not isinstance(data_item[0], QtGui.QStandardItem):
344            msg = "Incorrect type passed to the Fitting Perspective"
345            raise AttributeError(msg)
346
347        if is_batch:
348            # Just create a new fit tab. No empty batchFit tabs
349            self.addFit(data_item, is_batch=is_batch)
350            return
351
352        items = [data_item] if is_batch else data_item
353        for data in items:
354            # Find the first unassigned tab.
355            # If none, open a new tab.
356            available_tabs = [tab.acceptsData() for tab in self.tabs]
357
358            if numpy.any(available_tabs):
359                first_good_tab = available_tabs.index(True)
360                self.tabs[first_good_tab].data = data
361                tab_name = str(self.tabText(first_good_tab))
362                self.updateFitDict(data, tab_name)
363            else:
364                self.addFit(data, is_batch=is_batch)
365
366    def onFittingOptionsChange(self, fit_engine):
367        """
368        React to the fitting algorithm change by modifying window title
369        """
370        fitter = [f.id for f in options.FITTERS if f.name == str(fit_engine)][0]
371        # set the optimizer
372        self.fit_options.selected_id = str(fitter)
373        # Update the title
374        self.updateWindowTitle()
375
376    def onFittingStarted(self, tabs_for_fitting=None):
377        """
378        Notify tabs listed in tabs_for_fitting
379        that the fitting thread started
380        """
381        assert(isinstance(tabs_for_fitting, list))
382        assert(len(tabs_for_fitting)>0)
383
384        for tab_object in self.tabs:
385            if not isinstance(tab_object, FittingWidget):
386                continue
387            page_name = "Page%s"%tab_object.tab_id
388            if any([page_name in tab for tab in tabs_for_fitting]):
389                tab_object.disableInteractiveElements()
390
391        pass
392
393    def onFittingStopped(self, tabs_for_fitting=None):
394        """
395        Notify tabs listed in tabs_for_fitting
396        that the fitting thread stopped
397        """
398        assert(isinstance(tabs_for_fitting, list))
399        assert(len(tabs_for_fitting)>0)
400
401        for tab_object in self.tabs:
402            if not isinstance(tab_object, FittingWidget):
403                continue
404            page_name = "Page%s"%tab_object.tab_id
405            if any([page_name in tab for tab in tabs_for_fitting]):
406                tab_object.enableInteractiveElements()
407
408    def getCurrentStateAsXml(self):
409        """
410        Returns an XML version of the current state
411        """
412        state = {}
413        for tab in self.tabs:
414            pass
415        return state
416
417    @property
418    def currentTab(self):
419        """
420        Returns the tab widget currently shown
421        """
422        return self.currentWidget()
423
Note: See TracBrowser for help on using the repository browser.