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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since e5ae812 was e5ae812, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 10 months ago

Added C&S tab serialization.

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