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

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

Added tab specific information for the user. SASVIEW-1281

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