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

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

Analysis only for Fitting, project save on other perspectives saves
datasets. Fixed tab deletion on data removal for batch tabs.

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