source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingPerspective.py @ 04a269f

ESS_GUI_bumps_abstraction
Last change on this file since 04a269f was 04a269f, checked in by ibressler, 5 years ago

FittingOptions?: using new FittingMethods? structure, updated FittingPerspective?

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