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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 2ae3edb was 02e7d3a, checked in by Torin Cooper-Bennun <torin.cooper-bennun@…>, 6 years ago

fixed new fit page tabs not becoming the active widget after creation

  • Property mode set to 100644
File size: 10.7 KB
RevLine 
[6f7f652]1import numpy
[0efe791]2
[4992ff2]3from PyQt5 import QtCore
4from PyQt5 import QtGui
5from PyQt5 import QtWidgets
[4c7dd9f]6
[2d0e0c1]7from bumps import options
8from bumps import fitters
9
[3b3b40b]10import sas.qtgui.Utilities.LocalConfig as LocalConfig
[61a92d4]11import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
[5236449]12
[1bc27f1]13from sas.qtgui.Perspectives.Fitting.FittingWidget import FittingWidget
[676f137]14from sas.qtgui.Perspectives.Fitting.ConstraintWidget import ConstraintWidget
[2d0e0c1]15from sas.qtgui.Perspectives.Fitting.FittingOptions import FittingOptions
[06ce180]16from sas.qtgui.Perspectives.Fitting.GPUOptions import GPUOptions
[8bdb6f5]17
[4992ff2]18class FittingWindow(QtWidgets.QTabWidget):
[0efe791]19    """
20    """
[be8f4b0]21    tabsModifiedSignal = QtCore.pyqtSignal()
[14ec91c5]22    fittingStartedSignal = QtCore.pyqtSignal(list)
23    fittingStoppedSignal = QtCore.pyqtSignal(list)
24
[60af928]25    name = "Fitting" # For displaying in the combo box in DataExplorer
[811bec1]26    def __init__(self, parent=None, data=None):
[4992ff2]27
[f46f6dc]28        super(FittingWindow, self).__init__()
29
30        self.parent = parent
31        self._data = data
32
33        # List of active fits
34        self.tabs = []
35
36        # Max index for adding new, non-clashing tab names
[6b75aee]37        self.maxIndex = 1
[f46f6dc]38
[57be490]39        ## Index of the current tab
40        #self.currentTab = 0
[f46f6dc]41
[38eb433]42        # The default optimizer
[1bc27f1]43        self.optimizer = 'Levenberg-Marquardt'
[f46f6dc]44
[14ec91c5]45        # Dataset index -> Fitting tab mapping
[38eb433]46        self.dataToFitTab = {}
47
[f46f6dc]48        # The tabs need to be closeable
49        self.setTabsClosable(True)
[60af928]50
[14ec91c5]51        # The tabs need to be movabe
52        self.setMovable(True)
53
[811bec1]54        self.communicate = self.parent.communicator()
55
[60af928]56        # Initialize the first tab
[f46f6dc]57        self.addFit(None)
58
59        # Deal with signals
60        self.tabCloseRequested.connect(self.tabCloses)
[38eb433]61        self.communicate.dataDeletedSignal.connect(self.dataDeleted)
[14ec91c5]62        self.fittingStartedSignal.connect(self.onFittingStarted)
63        self.fittingStoppedSignal.connect(self.onFittingStopped)
[f46f6dc]64
[b1e36a3]65        # Perspective window not allowed to close by default
66        self._allow_close = False
67
[2d0e0c1]68        # Fit options - uniform for all tabs
69        self.fit_options = options.FIT_CONFIG
70        self.fit_options_widget = FittingOptions(self, config=self.fit_options)
71        self.fit_options.selected_id = fitters.LevenbergMarquardtFit.id
72
73        # Listen to GUI Manager signal updating fit options
74        self.fit_options_widget.fit_option_changed.connect(self.onFittingOptionsChange)
75
[06ce180]76        # GPU Options
[14fa542]77        self.gpu_options_widget = GPUOptions(self)
[06ce180]78
[2d0e0c1]79        self.updateWindowTitle()
80
81    def updateWindowTitle(self):
82        """
83        Update the window title with the current optimizer name
84        """
85        self.optimizer = self.fit_options.selected_name
[f46f6dc]86        self.setWindowTitle('Fit panel - Active Fitting Optimizer: %s' % self.optimizer)
[60af928]87
[2d0e0c1]88
[b1e36a3]89    def setClosable(self, value=True):
90        """
[14ec91c5]91        Allow outsiders to close this widget
[b1e36a3]92        """
93        assert isinstance(value, bool)
94
95        self._allow_close = value
96
97    def closeEvent(self, event):
98        """
99        Overwrite QDialog close method to allow for custom widget close
100        """
[2add354]101        # Invoke fit page events
[b1e36a3]102        if self._allow_close:
103            # reset the closability flag
104            self.setClosable(value=False)
[7c487846]105            # Tell the MdiArea to close the container
106            self.parentWidget().close()
[b1e36a3]107            event.accept()
108        else:
109            # Maybe we should just minimize
110            self.setWindowState(QtCore.Qt.WindowMinimized)
[2add354]111            event.ignore()
[b1e36a3]112
[ee18d33]113    def addFit(self, data, is_batch=False):
[60af928]114        """
115        Add a new tab for passed data
116        """
[1bc27f1]117        tab     = FittingWidget(parent=self.parent, data=data, tab_id=self.maxIndex+1)
[ee18d33]118        tab.is_batch_fitting = is_batch
[3b3b40b]119
[61a92d4]120        # Add this tab to the object library so it can be retrieved by scripting/jupyter
[676f137]121        tab_name = self.getTabName(is_batch=is_batch)
[38eb433]122        ObjectLibrary.addObject(tab_name, tab)
[f46f6dc]123        self.tabs.append(tab)
[38eb433]124        if data:
125            self.updateFitDict(data, tab_name)
[f46f6dc]126        self.maxIndex += 1
[3b3b40b]127        icon = QtGui.QIcon()
128        if is_batch:
129            icon.addPixmap(QtGui.QPixmap("src/sas/qtgui/images/icons/layers.svg"))
130        self.addTab(tab, icon, tab_name)
131        # Show the new tab
[02e7d3a]132        self.setCurrentWidget(tab);
[3b3b40b]133        # Notify listeners
[be8f4b0]134        self.tabsModifiedSignal.emit()
[38eb433]135
[676f137]136    def addConstraintTab(self):
137        """
138        Add a new C&S fitting tab
139        """
[14ec91c5]140        tabs = [isinstance(tab, ConstraintWidget) for tab in self.tabs]
141        if any(tabs):
142            # We already have a C&S tab: show it
143            self.setCurrentIndex(tabs.index(True))
144            return
145        tab     = ConstraintWidget(parent=self)
[676f137]146        # Add this tab to the object library so it can be retrieved by scripting/jupyter
147        tab_name = self.getCSTabName() # TODO update the tab name scheme
148        ObjectLibrary.addObject(tab_name, tab)
149        self.tabs.append(tab)
[3b3b40b]150        icon = QtGui.QIcon()
151        icon.addPixmap(QtGui.QPixmap("src/sas/qtgui/images/icons/link.svg"))
152        self.addTab(tab, icon, tab_name)
153
154        # This will be the last tab, so set the index accordingly
155        self.setCurrentIndex(self.count()-1)
[676f137]156
[38eb433]157    def updateFitDict(self, item_key, tab_name):
158        """
159        Create a list if none exists and append if there's already a list
160        """
[b3e8629]161        item_key_str = str(item_key)
162        if item_key_str in list(self.dataToFitTab.keys()):
163            self.dataToFitTab[item_key_str].append(tab_name)
[38eb433]164        else:
[b3e8629]165            self.dataToFitTab[item_key_str] = [tab_name]
[f46f6dc]166
[676f137]167    def getTabName(self, is_batch=False):
[f46f6dc]168        """
169        Get the new tab name, based on the number of fitting tabs so far
170        """
[38eb433]171        page_name = "BatchPage" if is_batch else "FitPage"
172        page_name = page_name + str(self.maxIndex)
[f46f6dc]173        return page_name
174
[676f137]175    def getCSTabName(self):
176        """
177        Get the new tab name, based on the number of fitting tabs so far
178        """
179        page_name = "Const. & Simul. Fit"
180        return page_name
181
[38eb433]182    def resetTab(self, index):
183        """
184        Adds a new tab and removes the last tab
185        as a way of resetting the fit tabs
186        """
187        # If data on tab empty - do nothing
[377ade1]188        if index in self.tabs and not self.tabs[index].data:
[38eb433]189            return
190        # Add a new, empy tab
191        self.addFit(None)
192        # Remove the previous last tab
193        self.tabCloses(index)
194
[f46f6dc]195    def tabCloses(self, index):
196        """
197        Update local bookkeeping on tab close
198        """
[38eb433]199        #assert len(self.tabs) >= index
[f46f6dc]200        # don't remove the last tab
201        if len(self.tabs) <= 1:
[38eb433]202            self.resetTab(index)
[f46f6dc]203            return
[38eb433]204        try:
205            ObjectLibrary.deleteObjectByRef(self.tabs[index])
206            self.removeTab(index)
207            del self.tabs[index]
[be8f4b0]208            self.tabsModifiedSignal.emit()
[38eb433]209        except IndexError:
210            # The tab might have already been deleted previously
211            pass
212
213    def closeTabByName(self, tab_name):
214        """
215        Given name of the fitting tab - close it
216        """
[b3e8629]217        for tab_index in range(len(self.tabs)):
[38eb433]218            if self.tabText(tab_index) == tab_name:
219                self.tabCloses(tab_index)
220        pass # debug hook
221
222    def dataDeleted(self, index_list):
223        """
224        Delete fit tabs referencing given data
225        """
226        if not index_list or not self.dataToFitTab:
227            return
228        for index_to_delete in index_list:
[b3e8629]229            index_to_delete_str = str(index_to_delete)
230            if index_to_delete_str in list(self.dataToFitTab.keys()):
231                for tab_name in self.dataToFitTab[index_to_delete_str]:
[38eb433]232                    # delete tab #index after corresponding data got removed
233                    self.closeTabByName(tab_name)
[b3e8629]234                self.dataToFitTab.pop(index_to_delete_str)
[38eb433]235
[f46f6dc]236    def allowBatch(self):
237        """
238        Tell the caller that we accept multiple data instances
239        """
240        return True
241
[ee18d33]242    def setData(self, data_item=None, is_batch=False):
[f46f6dc]243        """
244        Assign new dataset to the fitting instance
[5236449]245        Obtain a QStandardItem object and dissect it to get Data1D/2D
246        Pass it over to the calculator
[f46f6dc]247        """
[cbcdd2c]248        assert data_item is not None
[68c96d3]249
[5236449]250        if not isinstance(data_item, list):
251            msg = "Incorrect type passed to the Fitting Perspective"
[b3e8629]252            raise AttributeError(msg)
[5236449]253
254        if not isinstance(data_item[0], QtGui.QStandardItem):
255            msg = "Incorrect type passed to the Fitting Perspective"
[b3e8629]256            raise AttributeError(msg)
[5236449]257
[17968c3]258        if is_batch:
259            # Just create a new fit tab. No empty batchFit tabs
260            self.addFit(data_item, is_batch=is_batch)
261            return
[ee18d33]262
[17968c3]263        items = [data_item] if is_batch else data_item
[ee18d33]264        for data in items:
[454670d]265            # Find the first unassigned tab.
266            # If none, open a new tab.
[17968c3]267            available_tabs = [tab.acceptsData() for tab in self.tabs]
[454670d]268
269            if numpy.any(available_tabs):
[38eb433]270                first_good_tab = available_tabs.index(True)
271                self.tabs[first_good_tab].data = data
272                tab_name = str(self.tabText(first_good_tab))
273                self.updateFitDict(data, tab_name)
[454670d]274            else:
[ee18d33]275                self.addFit(data, is_batch=is_batch)
[2d0e0c1]276
[b0c5e8c]277    def onFittingOptionsChange(self, fit_engine):
[2d0e0c1]278        """
[b0c5e8c]279        React to the fitting algorithm change by modifying window title
[2d0e0c1]280        """
281        fitter = [f.id for f in options.FITTERS if f.name == str(fit_engine)][0]
282        # set the optimizer
283        self.fit_options.selected_id = str(fitter)
284        # Update the title
285        self.updateWindowTitle()
286
[14ec91c5]287    def onFittingStarted(self, tabs_for_fitting=None):
288        """
289        Notify tabs listed in tabs_for_fitting
290        that the fitting thread started
291        """
292        assert(isinstance(tabs_for_fitting, list))
293        assert(len(tabs_for_fitting)>0)
294
295        for tab_object in self.tabs:
296            if not isinstance(tab_object, FittingWidget):
297                continue
298            page_name = "Page%s"%tab_object.tab_id
299            if any([page_name in tab for tab in tabs_for_fitting]):
300                tab_object.setFittingStarted()
301
302        pass
303
304    def onFittingStopped(self, tabs_for_fitting=None):
305        """
306        Notify tabs listed in tabs_for_fitting
307        that the fitting thread stopped
308        """
309        assert(isinstance(tabs_for_fitting, list))
310        assert(len(tabs_for_fitting)>0)
311
312        for tab_object in self.tabs:
313            if not isinstance(tab_object, FittingWidget):
314                continue
315            page_name = "Page%s"%tab_object.tab_id
316            if any([page_name in tab for tab in tabs_for_fitting]):
317                tab_object.setFittingStopped()
318
[2d0e0c1]319        pass
[57be490]320
321    @property
322    def currentTab(self):
323        """
324        Returns the tab widget currently shown
325        """
326        return self.currentWidget()
327
Note: See TracBrowser for help on using the repository browser.