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

ESS_GUI_opencl
Last change on this file since e0d5b63 was 770c42c, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

Enabled clickable checkboxes for constraints

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