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

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

More batchpage related functionality for Analysis save/load

  • Property mode set to 100644
File size: 13.5 KB
Line 
1import numpy
2
3from PyQt5 import QtCore
4from PyQt5 import QtGui
5from PyQt5 import QtWidgets
6
7from bumps import options
8from bumps import fitters
9
10import sas.qtgui.Utilities.LocalConfig as LocalConfig
11import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
12import sas.qtgui.Utilities.GuiUtils as GuiUtils
13
14from sas.qtgui.Perspectives.Fitting.FittingWidget import FittingWidget
15from sas.qtgui.Perspectives.Fitting.ConstraintWidget import ConstraintWidget
16from sas.qtgui.Perspectives.Fitting.FittingOptions import FittingOptions
17from sas.qtgui.Perspectives.Fitting.GPUOptions import GPUOptions
18
19class FittingWindow(QtWidgets.QTabWidget):
20    """
21    """
22    tabsModifiedSignal = QtCore.pyqtSignal()
23    fittingStartedSignal = QtCore.pyqtSignal(list)
24    fittingStoppedSignal = QtCore.pyqtSignal(list)
25
26    name = "Fitting" # For displaying in the combo box in DataExplorer
27    def __init__(self, parent=None, data=None):
28
29        super(FittingWindow, self).__init__()
30
31        self.parent = parent
32        self._data = data
33
34        # List of active fits
35        self.tabs = []
36
37        # Max index for adding new, non-clashing tab names
38        self.maxIndex = 1
39
40        ## Index of the current tab
41        #self.currentTab = 0
42
43        # The default optimizer
44        self.optimizer = 'Levenberg-Marquardt'
45
46        # Dataset index -> Fitting tab mapping
47        self.dataToFitTab = {}
48
49        # The tabs need to be closeable
50        self.setTabsClosable(True)
51
52        # The tabs need to be movabe
53        self.setMovable(True)
54
55        self.communicate = self.parent.communicator()
56
57        # Initialize the first tab
58        self.addFit(None)
59
60        # Deal with signals
61        self.tabCloseRequested.connect(self.tabCloses)
62        self.communicate.dataDeletedSignal.connect(self.dataDeleted)
63        self.fittingStartedSignal.connect(self.onFittingStarted)
64        self.fittingStoppedSignal.connect(self.onFittingStopped)
65
66        self.communicate.copyFitParamsSignal.connect(self.onParamCopy)
67        self.communicate.pasteFitParamsSignal.connect(self.onParamPaste)
68        self.communicate.copyExcelFitParamsSignal.connect(self.onExcelCopy)
69        self.communicate.copyLatexFitParamsSignal.connect(self.onLatexCopy)
70
71
72        # Perspective window not allowed to close by default
73        self._allow_close = False
74
75        # Fit options - uniform for all tabs
76        self.fit_options = options.FIT_CONFIG
77        self.fit_options_widget = FittingOptions(self, config=self.fit_options)
78        self.fit_options.selected_id = fitters.LevenbergMarquardtFit.id
79
80        # Listen to GUI Manager signal updating fit options
81        self.fit_options_widget.fit_option_changed.connect(self.onFittingOptionsChange)
82
83        # GPU Options
84        self.gpu_options_widget = GPUOptions(self)
85
86        self.updateWindowTitle()
87
88    def updateWindowTitle(self):
89        """
90        Update the window title with the current optimizer name
91        """
92        self.optimizer = self.fit_options.selected_name
93        self.setWindowTitle('Fit panel - Active Fitting Optimizer: %s' % self.optimizer)
94
95
96    def setClosable(self, value=True):
97        """
98        Allow outsiders to close this widget
99        """
100        assert isinstance(value, bool)
101
102        self._allow_close = value
103
104    def onParamCopy(self):
105        self.currentTab.onCopyToClipboard("")
106
107    def onParamPaste(self):
108        self.currentTab.onParameterPaste()
109
110    def onExcelCopy(self):
111        self.currentTab.onCopyToClipboard("Excel")
112
113    def onLatexCopy(self):
114        self.currentTab.onCopyToClipboard("Latex")
115
116    def serializeAllFitpage(self):
117        # serialize all active fitpages and return
118        # a dictionary: {data_id: fitpage_state}
119        params = {}
120        for i, tab in enumerate(self.tabs):
121            tab_data = self.getSerializedFitpage(tab)
122            if tab.tab_id is None: continue
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            if index_to_delete_str in list(self.dataToFitTab.keys()):
309                for tab_name in self.dataToFitTab[index_to_delete_str]:
310                    # delete tab #index after corresponding data got removed
311                    self.closeTabByName(tab_name)
312                self.dataToFitTab.pop(index_to_delete_str)
313
314    def allowBatch(self):
315        """
316        Tell the caller that we accept multiple data instances
317        """
318        return True
319
320    def isSerializable(self):
321        """
322        Tell the caller that this perspective writes its state
323        """
324        return True
325
326    def setData(self, data_item=None, is_batch=False):
327        """
328        Assign new dataset to the fitting instance
329        Obtain a QStandardItem object and dissect it to get Data1D/2D
330        Pass it over to the calculator
331        """
332        assert data_item is not None
333
334        if not isinstance(data_item, list):
335            msg = "Incorrect type passed to the Fitting Perspective"
336            raise AttributeError(msg)
337
338        if not isinstance(data_item[0], QtGui.QStandardItem):
339            msg = "Incorrect type passed to the Fitting Perspective"
340            raise AttributeError(msg)
341
342        if is_batch:
343            # Just create a new fit tab. No empty batchFit tabs
344            self.addFit(data_item, is_batch=is_batch)
345            return
346
347        items = [data_item] if is_batch else data_item
348        for data in items:
349            # Find the first unassigned tab.
350            # If none, open a new tab.
351            available_tabs = [tab.acceptsData() for tab in self.tabs]
352
353            if numpy.any(available_tabs):
354                first_good_tab = available_tabs.index(True)
355                self.tabs[first_good_tab].data = data
356                tab_name = str(self.tabText(first_good_tab))
357                self.updateFitDict(data, tab_name)
358            else:
359                self.addFit(data, is_batch=is_batch)
360
361    def onFittingOptionsChange(self, fit_engine):
362        """
363        React to the fitting algorithm change by modifying window title
364        """
365        fitter = [f.id for f in options.FITTERS if f.name == str(fit_engine)][0]
366        # set the optimizer
367        self.fit_options.selected_id = str(fitter)
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        pass
404
405    def getCurrentStateAsXml(self):
406        """
407        Returns an XML version of the current state
408        """
409        state = {}
410        for tab in self.tabs:
411            pass
412        return state
413
414    @property
415    def currentTab(self):
416        """
417        Returns the tab widget currently shown
418        """
419        return self.currentWidget()
420
Note: See TracBrowser for help on using the repository browser.