source: sasview/src/sas/qtgui/Perspectives/Fitting/ConstraintWidget.py @ 2e5081b

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

Redone the widget slightly, to fit with requirements in trac#1135.
Added comboboxes for all available fitpage names
Removed constraint of needing two selected fitpages
Removed the "Swap" button - it is redundant now.
Added context dependence on the "Select All" action.
Removed immediate constraint validation to allow for more complex
formulas

  • Property mode set to 100644
File size: 35.1 KB
RevLine 
[731efec]1import logging
[c4c4957]2import copy
[676f137]3
[116dd4c1]4from twisted.internet import threads
5
[676f137]6import sas.qtgui.Utilities.GuiUtils as GuiUtils
[14ec91c5]7import sas.qtgui.Utilities.LocalConfig as LocalConfig
8
[676f137]9from PyQt5 import QtGui, QtCore, QtWidgets
10
[116dd4c1]11from sas.sascalc.fit.BumpsFitting import BumpsFit as Fit
[676f137]12
[116dd4c1]13import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
[676f137]14from sas.qtgui.Perspectives.Fitting.UI.ConstraintWidgetUI import Ui_ConstraintWidgetUI
[be8f4b0]15from sas.qtgui.Perspectives.Fitting.FittingWidget import FittingWidget
[116dd4c1]16from sas.qtgui.Perspectives.Fitting.FitThread import FitThread
17from sas.qtgui.Perspectives.Fitting.ConsoleUpdate import ConsoleUpdate
[2d466e4]18from sas.qtgui.Perspectives.Fitting.ComplexConstraint import ComplexConstraint
[14ec91c5]19from sas.qtgui.Perspectives.Fitting.Constraint import Constraint
[676f137]20
[72651df]21class DnDTableWidget(QtWidgets.QTableWidget):
22    def __init__(self, *args, **kwargs):
23        super().__init__(*args, **kwargs)
24
25        self.setDragEnabled(True)
26        self.setAcceptDrops(True)
27        self.viewport().setAcceptDrops(True)
28        self.setDragDropOverwriteMode(False)
29        self.setDropIndicatorShown(True)
30
31        self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
32        self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
33        self.setDragDropMode(QtWidgets.QAbstractItemView.InternalMove)
34
[2e5081b]35        self._is_dragged = False
[72651df]36
37    def isDragged(self):
38        """
39        Return the drag status
40        """
41        return self._is_dragged
42
43    def dragEnterEvent(self, event):
44        """
45        Called automatically on a drag in the TableWidget
46        """
47        self._is_dragged = True
48        event.accept()
49
50    def dragLeaveEvent(self, event):
51        """
52        Called automatically on a drag stop
53        """
54        self._is_dragged = False
55        event.accept()
56
57    def dropEvent(self, event: QtGui.QDropEvent):
58        if not event.isAccepted() and event.source() == self:
59            drop_row = self.drop_on(event)
60            rows = sorted(set(item.row() for item in self.selectedItems()))
61            rows_to_move = [[QtWidgets.QTableWidgetItem(self.item(row_index, column_index)) for column_index in range(self.columnCount())]
62                            for row_index in rows]
63            for row_index in reversed(rows):
64                self.removeRow(row_index)
65                if row_index < drop_row:
66                    drop_row -= 1
67
68            for row_index, data in enumerate(rows_to_move):
69                row_index += drop_row
70                self.insertRow(row_index)
71                for column_index, column_data in enumerate(data):
72                    self.setItem(row_index, column_index, column_data)
73            event.accept()
74            for row_index in range(len(rows_to_move)):
75                self.item(drop_row + row_index, 0).setSelected(True)
76                self.item(drop_row + row_index, 1).setSelected(True)
77        super().dropEvent(event)
78        # Reset the drag flag. Must be done after the drop even got accepted!
79        self._is_dragged = False
80
81    def drop_on(self, event):
82        index = self.indexAt(event.pos())
83        if not index.isValid():
84            return self.rowCount()
85
86        return index.row() + 1 if self.is_below(event.pos(), index) else index.row()
87
88    def is_below(self, pos, index):
89        rect = self.visualRect(index)
90        margin = 2
91        if pos.y() - rect.top() < margin:
92            return False
93        elif rect.bottom() - pos.y() < margin:
94            return True
95
96        return rect.contains(pos, True) and not \
97            (int(self.model().flags(index)) & QtCore.Qt.ItemIsDropEnabled) and \
98            pos.y() >= rect.center().y()
99
100
[676f137]101class ConstraintWidget(QtWidgets.QWidget, Ui_ConstraintWidgetUI):
102    """
[be8f4b0]103    Constraints Dialog to select the desired parameter/model constraints.
[676f137]104    """
[ecc5d043]105    fitCompleteSignal = QtCore.pyqtSignal(tuple)
[64b9e61]106    batchCompleteSignal = QtCore.pyqtSignal(tuple)
107    fitFailedSignal = QtCore.pyqtSignal(tuple)
[676f137]108
[14ec91c5]109    def __init__(self, parent=None):
[676f137]110        super(ConstraintWidget, self).__init__()
[72651df]111
[676f137]112        self.parent = parent
113        self.setupUi(self)
114        self.currentType = "FitPage"
[116dd4c1]115        # Page id for fitting
116        # To keep with previous SasView values, use 300 as the start offset
[14ec91c5]117        self.page_id = 301
[e5ae812]118        self.tab_id = self.page_id
[72651df]119        # fitpage order in the widget
120        self._row_order = []
121
122        # Set the table widget into layout
123        self.tblTabList = DnDTableWidget(self)
124        self.tblLayout.addWidget(self.tblTabList)
[676f137]125
[91ad45c]126        # Are we chain fitting?
127        self.is_chain_fitting = False
128
[47d7d2d]129        # Remember previous content of modified cell
130        self.current_cell = ""
131
[ba01ad1]132        # Tabs used in simultaneous fitting
133        # tab_name : True/False
134        self.tabs_for_fitting = {}
135
[be8f4b0]136        # Set up the widgets
137        self.initializeWidgets()
138
[676f137]139        # Set up signals/slots
140        self.initializeSignals()
141
142        # Create the list of tabs
[be8f4b0]143        self.initializeFitList()
144
145    def acceptsData(self):
146        """ Tells the caller this widget doesn't accept data """
147        return False
148
149    def initializeWidgets(self):
150        """
151        Set up various widget states
152        """
[64b9e61]153        # disable special cases until properly defined
154        self.label.setVisible(False)
155        self.cbCases.setVisible(False)
156
[47d7d2d]157        labels = ['FitPage', 'Model', 'Data', 'Mnemonic']
[be8f4b0]158        # tab widget - headers
[47d7d2d]159        self.editable_tab_columns = [labels.index('Mnemonic')]
[be8f4b0]160        self.tblTabList.setColumnCount(len(labels))
161        self.tblTabList.setHorizontalHeaderLabels(labels)
162        self.tblTabList.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
163
164        self.tblTabList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
165        self.tblTabList.customContextMenuRequested.connect(self.showModelContextMenu)
166
[91ad45c]167        # Single Fit is the default, so disable chainfit
168        self.chkChain.setVisible(False)
169
[be8f4b0]170        # disabled constraint
171        labels = ['Constraint']
172        self.tblConstraints.setColumnCount(len(labels))
173        self.tblConstraints.setHorizontalHeaderLabels(labels)
174        self.tblConstraints.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
175        self.tblConstraints.setEnabled(False)
[09e0c32]176        header = self.tblConstraints.horizontalHeaderItem(0)
[64b9e61]177        header.setToolTip("Double click a row below to edit the constraint.")
[be8f4b0]178
179        self.tblConstraints.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
180        self.tblConstraints.customContextMenuRequested.connect(self.showConstrContextMenu)
[676f137]181
182    def initializeSignals(self):
183        """
184        Set up signals/slots for this widget
185        """
[47d7d2d]186        # simple widgets
[676f137]187        self.btnSingle.toggled.connect(self.onFitTypeChange)
188        self.btnBatch.toggled.connect(self.onFitTypeChange)
[be8f4b0]189        self.cbCases.currentIndexChanged.connect(self.onSpecialCaseChange)
[676f137]190        self.cmdFit.clicked.connect(self.onFit)
191        self.cmdHelp.clicked.connect(self.onHelp)
[ecc5d043]192        self.cmdAdd.clicked.connect(self.showMultiConstraint)
[91ad45c]193        self.chkChain.toggled.connect(self.onChainFit)
[47d7d2d]194
195        # QTableWidgets
[ba01ad1]196        self.tblTabList.cellChanged.connect(self.onTabCellEdit)
[47d7d2d]197        self.tblTabList.cellDoubleClicked.connect(self.onTabCellEntered)
198        self.tblConstraints.cellChanged.connect(self.onConstraintChange)
199
[ecc5d043]200        # Internal signals
201        self.fitCompleteSignal.connect(self.fitComplete)
[64b9e61]202        self.batchCompleteSignal.connect(self.batchComplete)
203        self.fitFailedSignal.connect(self.fitFailed)
[ecc5d043]204
[47d7d2d]205        # External signals
[a90c9c5]206        self.parent.tabsModifiedSignal.connect(self.initializeFitList)
[be8f4b0]207
208    def updateSignalsFromTab(self, tab=None):
209        """
210        Intercept update signals from fitting tabs
211        """
[731efec]212        if tab is None:
213            return
214        tab_object = ObjectLibrary.getObject(tab)
215
216        # Disconnect all local slots
[3b3b40b]217        #tab_object.disconnect()
[731efec]218
219        # Reconnect tab signals to local slots
220        tab_object.constraintAddedSignal.connect(self.initializeFitList)
221        tab_object.newModelSignal.connect(self.initializeFitList)
[676f137]222
223    def onFitTypeChange(self, checked):
224        """
225        Respond to the fit type change
226        single fit/batch fit
227        """
[ba01ad1]228        source = self.sender().objectName()
229        self.currentType = "BatchPage" if source == "btnBatch" else "FitPage"
[91ad45c]230        self.chkChain.setVisible(source=="btnBatch")
[ba01ad1]231        self.initializeFitList()
[676f137]232
233    def onSpecialCaseChange(self, index):
234        """
235        Respond to the combobox change for special case constraint sets
236        """
237        pass
238
[14ec91c5]239    def getTabsForFit(self):
240        """
241        Returns list of tab names selected for fitting
242        """
243        return [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
244
[91ad45c]245    def onChainFit(self, is_checked):
246        """
247        Respond to selecting the Chain Fit checkbox
248        """
249        self.is_chain_fitting = is_checked
250
[676f137]251    def onFit(self):
252        """
253        Perform the constrained/simultaneous fit
254        """
[116dd4c1]255        # Find out all tabs to fit
[14ec91c5]256        tabs_to_fit = self.getTabsForFit()
[116dd4c1]257
258        # Single fitter for the simultaneous run
[6ca0da0]259        fitter = Fit()
260        fitter.fitter_id = self.page_id
[116dd4c1]261
262        # prepare fitting problems for each tab
263        #
264        page_ids = []
265        fitter_id = 0
[6ca0da0]266        sim_fitter_list=[fitter]
[c6343a5]267        # Prepare the fitter object
268        try:
269            for tab in tabs_to_fit:
[64b9e61]270                if not self.isTabImportable(tab): continue
[c6343a5]271                tab_object = ObjectLibrary.getObject(tab)
272                if tab_object is None:
273                    # No such tab!
274                    return
[8b480d27]275                sim_fitter_list, fitter_id = \
276                    tab_object.prepareFitters(fitter=sim_fitter_list[0], fit_id=fitter_id)
[c6343a5]277                page_ids.append([tab_object.page_id])
[6ca0da0]278        except ValueError:
[c6343a5]279            # No parameters selected in one of the tabs
[ecc5d043]280            no_params_msg = "Fitting cannot be performed.\n" +\
[c6343a5]281                            "Not all tabs chosen for fitting have parameters selected for fitting."
[e4c475b7]282            QtWidgets.QMessageBox.warning(self,
[3b3b40b]283                                          'Warning',
284                                           no_params_msg,
285                                           QtWidgets.QMessageBox.Ok)
[c6343a5]286
287            return
[116dd4c1]288
289        # Create the fitting thread, based on the fitter
290        completefn = self.onBatchFitComplete if self.currentType=='BatchPage' else self.onFitComplete
291
[14ec91c5]292        if LocalConfig.USING_TWISTED:
293            handler = None
294            updater = None
295        else:
296            handler = ConsoleUpdate(parent=self.parent,
297                                    manager=self,
298                                    improvement_delta=0.1)
299            updater = handler.update_fit
[116dd4c1]300
301        batch_inputs = {}
302        batch_outputs = {}
303
[ecc5d043]304        # Notify the parent about fitting started
305        self.parent.fittingStartedSignal.emit(tabs_to_fit)
306
[116dd4c1]307        # new fit thread object
308        calc_fit = FitThread(handler=handler,
[6ca0da0]309                             fn=sim_fitter_list,
[116dd4c1]310                             batch_inputs=batch_inputs,
311                             batch_outputs=batch_outputs,
312                             page_id=page_ids,
313                             updatefn=updater,
[91ad45c]314                             completefn=completefn,
315                             reset_flag=self.is_chain_fitting)
[116dd4c1]316
[14ec91c5]317        if LocalConfig.USING_TWISTED:
318            # start the trhrhread with twisted
319            calc_thread = threads.deferToThread(calc_fit.compute)
320            calc_thread.addCallback(completefn)
321            calc_thread.addErrback(self.onFitFailed)
322        else:
323            # Use the old python threads + Queue
324            calc_fit.queue()
325            calc_fit.ready(2.5)
[116dd4c1]326
327
328        #disable the Fit button
[64b9e61]329        self.cmdFit.setStyleSheet('QPushButton {color: red;}')
[116dd4c1]330        self.cmdFit.setText('Running...')
331        self.parent.communicate.statusBarUpdateSignal.emit('Fitting started...')
332        self.cmdFit.setEnabled(False)
[676f137]333
334    def onHelp(self):
335        """
[c6343a5]336        Show the "Fitting" section of help
[676f137]337        """
[aed0532]338        tree_location = "/user/qtgui/Perspectives/Fitting/"
[c6343a5]339
340        helpfile = "fitting_help.html#simultaneous-fit-mode"
341        help_location = tree_location + helpfile
342
343        # OMG, really? Crawling up the object hierarchy...
344        self.parent.parent.showHelp(help_location)
[676f137]345
[ba01ad1]346    def onTabCellEdit(self, row, column):
[47d7d2d]347        """
[ba01ad1]348        Respond to check/uncheck and to modify the model moniker actions
[47d7d2d]349        """
[72651df]350        # If this "Edit" is just a response from moving rows around,
351        # update the tab order and leave
352        if self.tblTabList.isDragged():
353            self._row_order = []
354            for i in range(self.tblTabList.rowCount()):
355                self._row_order.append(self.tblTabList.item(i,0).data(0))
356            return
357
[ba01ad1]358        item = self.tblTabList.item(row, column)
359        if column == 0:
360            # Update the tabs for fitting list
361            tab_name = item.text()
362            self.tabs_for_fitting[tab_name] = (item.checkState() == QtCore.Qt.Checked)
363            # Enable fitting only when there are models to fit
364            self.cmdFit.setEnabled(any(self.tabs_for_fitting.values()))
365
[47d7d2d]366        if column not in self.editable_tab_columns:
367            return
368        new_moniker = item.data(0)
369
370        # The new name should be validated on the fly, with QValidator
371        # but let's just assure it post-factum
372        is_good_moniker = self.validateMoniker(new_moniker)
373        if not is_good_moniker:
[eb1a386]374            self.tblTabList.blockSignals(True)
[47d7d2d]375            item.setBackground(QtCore.Qt.red)
[eb1a386]376            self.tblTabList.blockSignals(False)
[47d7d2d]377            self.cmdFit.setEnabled(False)
[72651df]378            if new_moniker == "":
379                msg = "Please use a non-empty name."
380            else:
381                msg = "Please use a unique name."
382            self.parent.communicate.statusBarUpdateSignal.emit(msg)
383            item.setToolTip(msg)
[ba01ad1]384            return
385        self.tblTabList.blockSignals(True)
386        item.setBackground(QtCore.Qt.white)
387        self.tblTabList.blockSignals(False)
388        self.cmdFit.setEnabled(True)
[72651df]389        item.setToolTip("")
390        msg = "Fitpage name changed to {}.".format(new_moniker)
391        self.parent.communicate.statusBarUpdateSignal.emit(msg)
392
[ba01ad1]393        if not self.current_cell:
394            return
395        # Remember the value
396        if self.current_cell not in self.available_tabs:
397            return
398        temp_tab = self.available_tabs[self.current_cell]
399        # Remove the key from the dictionaries
400        self.available_tabs.pop(self.current_cell, None)
401        # Change the model name
402        model = temp_tab.kernel_module
403        model.name = new_moniker
404        # Replace constraint name
405        temp_tab.replaceConstraintName(self.current_cell, new_moniker)
[0764593]406        # Replace constraint name in the remaining tabs
407        for tab in self.available_tabs.values():
408            tab.replaceConstraintName(self.current_cell, new_moniker)
[ba01ad1]409        # Reinitialize the display
410        self.initializeFitList()
[47d7d2d]411
412    def onConstraintChange(self, row, column):
413        """
[116dd4c1]414        Modify the constraint's "active" instance variable.
[47d7d2d]415        """
[ba01ad1]416        item = self.tblConstraints.item(row, column)
[ecc5d043]417        if column != 0: return
418        # Update the tabs for fitting list
419        constraint = self.available_constraints[row]
420        constraint.active = (item.checkState() == QtCore.Qt.Checked)
421        # Update the constraint formula
422        constraint = self.available_constraints[row]
423        function = item.text()
424        # remove anything left of '=' to get the constraint
425        function = function[function.index('=')+1:]
426        # No check on function here - trust the user (R)
427        if function != constraint.func:
[09e0c32]428            # This becomes rather difficult to validate now.
429            # Turn off validation for Edit Constraint
[ecc5d043]430            constraint.func = function
[09e0c32]431            constraint.validate = False
[47d7d2d]432
433    def onTabCellEntered(self, row, column):
434        """
435        Remember the original tab list cell data.
436        Needed for reverting back on bad validation
437        """
438        if column != 3:
439            return
440        self.current_cell = self.tblTabList.item(row, column).data(0)
441
[116dd4c1]442    def onFitComplete(self, result):
443        """
[ecc5d043]444        Send the fit complete signal to main thread
445        """
446        self.fitCompleteSignal.emit(result)
447
448    def fitComplete(self, result):
449        """
[116dd4c1]450        Respond to the successful fit complete signal
451        """
[17968c3]452        #re-enable the Fit button
[64b9e61]453        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
[17968c3]454        self.cmdFit.setText("Fit")
455        self.cmdFit.setEnabled(True)
456
[14ec91c5]457        # Notify the parent about completed fitting
458        self.parent.fittingStoppedSignal.emit(self.getTabsForFit())
459
[235d766]460        # Assure the fitting succeeded
461        if result is None or not result:
462            msg = "Fitting failed. Please ensure correctness of chosen constraints."
463            self.parent.communicate.statusBarUpdateSignal.emit(msg)
464            return
465
[c6343a5]466        # get the elapsed time
467        elapsed = result[1]
468
469        # result list
470        results = result[0][0]
471
472        # Find out all tabs to fit
473        tabs_to_fit = [tab for tab in self.tabs_for_fitting if self.tabs_for_fitting[tab]]
474
475        # update all involved tabs
476        for i, tab in enumerate(tabs_to_fit):
477            tab_object = ObjectLibrary.getObject(tab)
478            if tab_object is None:
479                # No such tab. removed while job was running
480                return
481            # Make sure result and target objects are the same (same model moniker)
482            if tab_object.kernel_module.name == results[i].model.name:
483                tab_object.fitComplete(([[results[i]]], elapsed))
[116dd4c1]484
[17968c3]485        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
486        self.parent.communicate.statusBarUpdateSignal.emit(msg)
487
[116dd4c1]488    def onBatchFitComplete(self, result):
489        """
[64b9e61]490        Send the fit complete signal to main thread
491        """
492        self.batchCompleteSignal.emit(result)
493
494    def batchComplete(self, result):
495        """
[116dd4c1]496        Respond to the successful batch fit complete signal
497        """
[17968c3]498        #re-enable the Fit button
[64b9e61]499        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
[17968c3]500        self.cmdFit.setText("Fit")
501        self.cmdFit.setEnabled(True)
502
[14ec91c5]503        # Notify the parent about completed fitting
504        self.parent.fittingStoppedSignal.emit(self.getTabsForFit())
505
[17968c3]506        # get the elapsed time
507        elapsed = result[1]
508
[d4dac80]509        if result is None:
510            msg = "Fitting failed."
511            self.parent.communicate.statusBarUpdateSignal.emit(msg)
512            return
513
514        # Show the grid panel
[c4c4957]515        page_name = "ConstSimulPage"
516        results = copy.deepcopy(result[0])
517        results.append(page_name)
518        self.parent.communicate.sendDataToGridSignal.emit(results)
[17968c3]519
520        msg = "Fitting completed successfully in: %s s.\n" % GuiUtils.formatNumber(elapsed)
521        self.parent.communicate.statusBarUpdateSignal.emit(msg)
522
[116dd4c1]523    def onFitFailed(self, reason):
524        """
[64b9e61]525        Send the fit failed signal to main thread
526        """
527        self.fitFailedSignal.emit(result)
528
529    def fitFailed(self, reason):
530        """
[17968c3]531        Respond to fitting failure.
[116dd4c1]532        """
[17968c3]533        #re-enable the Fit button
[64b9e61]534        self.cmdFit.setStyleSheet('QPushButton {color: black;}')
[17968c3]535        self.cmdFit.setText("Fit")
536        self.cmdFit.setEnabled(True)
537
[14ec91c5]538        # Notify the parent about completed fitting
539        self.parent.fittingStoppedSignal.emit(self.getTabsForFit())
540
[17968c3]541        msg = "Fitting failed: %s s.\n" % reason
542        self.parent.communicate.statusBarUpdateSignal.emit(msg)
[ecc5d043]543
[be8f4b0]544    def isTabImportable(self, tab):
[676f137]545        """
[be8f4b0]546        Determines if the tab can be imported and included in the widget
[676f137]547        """
[da9a0722]548        if not isinstance(tab, str): return False
[be8f4b0]549        if not self.currentType in tab: return False
550        object = ObjectLibrary.getObject(tab)
551        if not isinstance(object, FittingWidget): return False
[91ad45c]552        if not object.data_is_loaded : return False
[be8f4b0]553        return True
554
555    def showModelContextMenu(self, position):
556        """
557        Show context specific menu in the tab table widget.
558        """
559        menu = QtWidgets.QMenu()
560        rows = [s.row() for s in self.tblTabList.selectionModel().selectedRows()]
561        num_rows = len(rows)
562        if num_rows <= 0:
[676f137]563            return
[be8f4b0]564        # Select for fitting
565        param_string = "Fit Page " if num_rows==1 else "Fit Pages "
[676f137]566
[be8f4b0]567        self.actionSelect = QtWidgets.QAction(self)
568        self.actionSelect.setObjectName("actionSelect")
569        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
570        # Unselect from fitting
571        self.actionDeselect = QtWidgets.QAction(self)
572        self.actionDeselect.setObjectName("actionDeselect")
573        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
574
575        self.actionRemoveConstraint = QtWidgets.QAction(self)
576        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
577        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove all constraints on selected models"))
578
579        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
580        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
581        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of parameters in selected models..."))
582
583        menu.addAction(self.actionSelect)
584        menu.addAction(self.actionDeselect)
585        menu.addSeparator()
586
587        if num_rows >= 2:
588            menu.addAction(self.actionMutualMultiConstrain)
[676f137]589
[be8f4b0]590        # Define the callbacks
[47d7d2d]591        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
[be8f4b0]592        self.actionSelect.triggered.connect(self.selectModels)
593        self.actionDeselect.triggered.connect(self.deselectModels)
594        try:
595            menu.exec_(self.tblTabList.viewport().mapToGlobal(position))
596        except AttributeError as ex:
597            logging.error("Error generating context menu: %s" % ex)
598        return
599
600    def showConstrContextMenu(self, position):
[676f137]601        """
[be8f4b0]602        Show context specific menu in the tab table widget.
[676f137]603        """
[be8f4b0]604        menu = QtWidgets.QMenu()
605        rows = [s.row() for s in self.tblConstraints.selectionModel().selectedRows()]
606        num_rows = len(rows)
607        if num_rows <= 0:
608            return
609        # Select for fitting
610        param_string = "constraint " if num_rows==1 else "constraints "
611
612        self.actionSelect = QtWidgets.QAction(self)
613        self.actionSelect.setObjectName("actionSelect")
614        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
615        # Unselect from fitting
616        self.actionDeselect = QtWidgets.QAction(self)
617        self.actionDeselect.setObjectName("actionDeselect")
618        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
[676f137]619
[be8f4b0]620        self.actionRemoveConstraint = QtWidgets.QAction(self)
621        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
622        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove "+param_string))
623
624        menu.addAction(self.actionSelect)
625        menu.addAction(self.actionDeselect)
626        menu.addSeparator()
627        menu.addAction(self.actionRemoveConstraint)
628
629        # Define the callbacks
630        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
631        self.actionSelect.triggered.connect(self.selectConstraints)
632        self.actionDeselect.triggered.connect(self.deselectConstraints)
633        try:
634            menu.exec_(self.tblConstraints.viewport().mapToGlobal(position))
635        except AttributeError as ex:
636            logging.error("Error generating context menu: %s" % ex)
637        return
638
639    def selectConstraints(self):
640        """
641        Selected constraints are chosen for fitting
642        """
643        status = QtCore.Qt.Checked
644        self.setRowSelection(self.tblConstraints, status)
645
646    def deselectConstraints(self):
647        """
648        Selected constraints are removed for fitting
649        """
650        status = QtCore.Qt.Unchecked
651        self.setRowSelection(self.tblConstraints, status)
652
653    def selectModels(self):
654        """
655        Selected models are chosen for fitting
656        """
657        status = QtCore.Qt.Checked
658        self.setRowSelection(self.tblTabList, status)
659
660    def deselectModels(self):
661        """
662        Selected models are removed for fitting
663        """
664        status = QtCore.Qt.Unchecked
665        self.setRowSelection(self.tblTabList, status)
666
667    def selectedParameters(self, widget):
668        """ Returns list of selected (highlighted) parameters """
669        return [s.row() for s in widget.selectionModel().selectedRows()]
670
671    def setRowSelection(self, widget, status=QtCore.Qt.Unchecked):
672        """
673        Selected models are chosen for fitting
674        """
675        # Convert to proper indices and set requested enablement
676        for row in self.selectedParameters(widget):
677            widget.item(row, 0).setCheckState(status)
678
679    def deleteConstraint(self):#, row):
680        """
681        Delete all selected constraints.
682        """
[47d7d2d]683        # Removing rows from the table we're iterating over,
684        # so prepare a list of data first
[be8f4b0]685        constraints_to_delete = []
686        for row in self.selectedParameters(self.tblConstraints):
687            constraints_to_delete.append(self.tblConstraints.item(row, 0).data(0))
688        for constraint in constraints_to_delete:
689            moniker = constraint[:constraint.index(':')]
690            param = constraint[constraint.index(':')+1:constraint.index('=')].strip()
691            tab = self.available_tabs[moniker]
692            tab.deleteConstraintOnParameter(param)
693        # Constraints removed - refresh the table widget
694        self.initializeFitList()
695
[47d7d2d]696    def uneditableItem(self, data=""):
697        """
698        Returns an uneditable Table Widget Item
699        """
700        item = QtWidgets.QTableWidgetItem(data)
701        item.setFlags( QtCore.Qt.ItemIsSelectable |  QtCore.Qt.ItemIsEnabled )
702        return item
703
[be8f4b0]704    def updateFitLine(self, tab):
705        """
706        Update a single line of the table widget with tab info
707        """
[ba01ad1]708        fit_page = ObjectLibrary.getObject(tab)
709        model = fit_page.kernel_module
[be8f4b0]710        if model is None:
711            return
712        tab_name = tab
713        model_name = model.id
714        moniker = model.name
[ba01ad1]715        model_data = fit_page.data
[be8f4b0]716        model_filename = model_data.filename
[ba01ad1]717        self.available_tabs[moniker] = fit_page
[be8f4b0]718
719        # Update the model table widget
720        pos = self.tblTabList.rowCount()
721        self.tblTabList.insertRow(pos)
[47d7d2d]722        item = self.uneditableItem(tab_name)
723        item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
[ba01ad1]724        if tab_name in self.tabs_for_fitting:
725            state = QtCore.Qt.Checked if self.tabs_for_fitting[tab_name] else QtCore.Qt.Unchecked
726            item.setCheckState(state)
727        else:
728            item.setCheckState(QtCore.Qt.Checked)
729            self.tabs_for_fitting[tab_name] = True
730
[731efec]731        # Disable signals so we don't get infinite call recursion
732        self.tblTabList.blockSignals(True)
[be8f4b0]733        self.tblTabList.setItem(pos, 0, item)
[47d7d2d]734        self.tblTabList.setItem(pos, 1, self.uneditableItem(model_name))
735        self.tblTabList.setItem(pos, 2, self.uneditableItem(model_filename))
736        # Moniker is editable, so no option change
737        item = QtWidgets.QTableWidgetItem(moniker)
738        self.tblTabList.setItem(pos, 3, item)
739        self.tblTabList.blockSignals(False)
[be8f4b0]740
741        # Check if any constraints present in tab
[235d766]742        constraint_names = fit_page.getComplexConstraintsForModel()
[ba01ad1]743        constraints = fit_page.getConstraintObjectsForModel()
[be8f4b0]744        if not constraints: 
745            return
746        self.tblConstraints.setEnabled(True)
[731efec]747        self.tblConstraints.blockSignals(True)
[ba01ad1]748        for constraint, constraint_name in zip(constraints, constraint_names):
[be8f4b0]749            # Create the text for widget item
[ba01ad1]750            label = moniker + ":"+ constraint_name[0] + " = " + constraint_name[1]
751            pos = self.tblConstraints.rowCount()
752            self.available_constraints[pos] = constraint
[be8f4b0]753
754            # Show the text in the constraint table
[ba01ad1]755            item = self.uneditableItem(label)
[ecc5d043]756            item = QtWidgets.QTableWidgetItem(label)
[ba01ad1]757            item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
[be8f4b0]758            item.setCheckState(QtCore.Qt.Checked)
759            self.tblConstraints.insertRow(pos)
760            self.tblConstraints.setItem(pos, 0, item)
[731efec]761        self.tblConstraints.blockSignals(False)
[be8f4b0]762
763    def initializeFitList(self):
764        """
765        Fill the list of model/data sets for fitting/constraining
766        """
767        # look at the object library to find all fit tabs
768        # Show the content of the current "model"
769        objects = ObjectLibrary.listObjects()
770
771        # Tab dict
772        # moniker -> (kernel_module, data)
773        self.available_tabs = {}
774        # Constraint dict
775        # moniker -> [constraints]
776        self.available_constraints = {}
777
778        # Reset the table widgets
779        self.tblTabList.setRowCount(0)
780        self.tblConstraints.setRowCount(0)
781
782        # Fit disabled
783        self.cmdFit.setEnabled(False)
784
785        if not objects:
786            return
[676f137]787
[be8f4b0]788        tabs = [tab for tab in ObjectLibrary.listObjects() if self.isTabImportable(tab)]
[72651df]789        if not self._row_order:
790            # Initialize tab order list
791            self._row_order = tabs
792        else:
793            tabs = self.orderedSublist(self._row_order, tabs)
794            self._row_order = tabs
795
[be8f4b0]796        for tab in tabs:
797            self.updateFitLine(tab)
798            self.updateSignalsFromTab(tab)
799            # We have at least 1 fit page, allow fitting
800            self.cmdFit.setEnabled(True)
[47d7d2d]801
[72651df]802    def orderedSublist(self, order_list, target_list):
803        """
804        Orders the target_list such that any elements
805        present in order_list show up first and in the order
806        from order_list.
807        """
808        tmp_list = []
809        # 1. get the non-matching elements
810        nonmatching = list(set(target_list) - set(order_list))
811        # 2: start with matching tabs, in the correct order
812        for elem in order_list:
813            if elem in target_list:
814                tmp_list.append(elem)
815        # 3. add the remaning tabs in any order
816        ordered_list = tmp_list + nonmatching
817        return ordered_list
818
[47d7d2d]819    def validateMoniker(self, new_moniker=None):
820        """
821        Check new_moniker for correctness.
822        It must be non-empty.
823        It must not be the same as other monikers.
824        """
825        if not new_moniker:
826            return False
827
828        for existing_moniker in self.available_tabs:
829            if existing_moniker == new_moniker and existing_moniker != self.current_cell:
830                return False
831
832        return True
833
[c5a2722f]834    def getObjectByName(self, name):
[731efec]835        """
836        Given name of the fit, returns associated fit object
837        """
[c5a2722f]838        for object_name in ObjectLibrary.listObjects():
839            object = ObjectLibrary.getObject(object_name)
840            if isinstance(object, FittingWidget):
841                try:
842                    if object.kernel_module.name == name:
843                        return object
844                except AttributeError:
845                    # Disregard atribute errors - empty fit widgets
846                    continue
847        return None
848
[ecc5d043]849    def onAcceptConstraint(self, con_tuple):
850        """
851        Receive constraint tuple from the ComplexConstraint dialog and adds contraint
852        """
853        #"M1, M2, M3" etc
854        model_name, constraint = con_tuple
855        constrained_tab = self.getObjectByName(model_name)
856        if constrained_tab is None:
857            return
858
859        # Find the constrained parameter row
860        constrained_row = constrained_tab.getRowFromName(constraint.param)
861
862        # Update the tab
863        constrained_tab.addConstraintToRow(constraint, constrained_row)
864
[442a9ae]865        # Select this parameter for adjusting/fitting
866        constrained_tab.selectCheckbox(constrained_row)
867
868
[47d7d2d]869    def showMultiConstraint(self):
870        """
871        Invoke the complex constraint editor
872        """
[2d466e4]873        selected_rows = self.selectedParameters(self.tblTabList)
874
[2e5081b]875        tab_list = [ObjectLibrary.getObject(self.tblTabList.item(s, 0).data(0)) for s in range(self.tblTabList.rowCount())]
[2d466e4]876        # Create and display the widget for param1 and param2
877        cc_widget = ComplexConstraint(self, tabs=tab_list)
[ecc5d043]878        cc_widget.constraintReadySignal.connect(self.onAcceptConstraint)
[2d466e4]879
880        if cc_widget.exec_() != QtWidgets.QDialog.Accepted:
881            return
882
[e5ae812]883    def getFitPage(self):
884        """
885        Retrieves the state of this page
886        """
887        param_list = []
[c5a2722f]888
[e5ae812]889        param_list.append(['is_constraint', 'True'])
890        param_list.append(['data_id', "cs_tab"+str(self.page_id)])
891        param_list.append(['current_type', self.currentType])
892        param_list.append(['is_chain_fitting', str(self.is_chain_fitting)])
893        param_list.append(['special_case', self.cbCases.currentText()])
[3b3b40b]894
[e5ae812]895        return param_list
[c5a2722f]896
[e5ae812]897    def getFitModel(self):
898        """
899        Retrieves current model
900        """
901        model_list = []
[c5a2722f]902
[e5ae812]903        checked_models = {}
904        for row in range(self.tblTabList.rowCount()):
905            model_name = self.tblTabList.item(row,1).data(0)
906            active = self.tblTabList.item(row,0).checkState()# == QtCore.Qt.Checked
907            checked_models[model_name] = str(active)
908
909        checked_constraints = {}
910        for row in range(self.tblConstraints.rowCount()):
911            model_name = self.tblConstraints.item(row,0).data(0)
912            active = self.tblConstraints.item(row,0).checkState()# == QtCore.Qt.Checked
913            checked_constraints[model_name] = str(active)
914
915        model_list.append(['checked_models', checked_models])
916        model_list.append(['checked_constraints', checked_constraints])
917        return model_list
918
919    def createPageForParameters(self, parameters=None):
920        """
921        Update the page with passed parameter values
922        """
923        # checked models
924        if not 'checked_models' in parameters:
925            return
926        models = parameters['checked_models'][0]
927        for model, check_state in models.items():
928            for row in range(self.tblTabList.rowCount()):
929                model_name = self.tblTabList.item(row,1).data(0)
930                if model_name != model:
931                    continue
932                # check/uncheck item
933                self.tblTabList.item(row,0).setCheckState(int(check_state))
934
935        if not 'checked_constraints' in parameters:
936            return
937        # checked constraints
938        models = parameters['checked_constraints'][0]
939        for model, check_state in models.items():
940            for row in range(self.tblConstraints.rowCount()):
941                model_name = self.tblConstraints.item(row,0).data(0)
942                if model_name != model:
943                    continue
944                # check/uncheck item
945                self.tblConstraints.item(row,0).setCheckState(int(check_state))
946
947        # fit/batch radio
948        isBatch = parameters['current_type'][0] == 'BatchPage'
949        if isBatch:
950            self.btnBatch.toggle()
951
952        # chain
953        is_chain = parameters['is_chain_fitting'][0] == 'True'
954        if isBatch:
955            self.chkChain.setChecked(is_chain)
Note: See TracBrowser for help on using the repository browser.