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

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since c5a2722f was c5a2722f, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Complex constraint widget updating the main constraint widget and fitting tabs

  • Property mode set to 100644
File size: 17.7 KB
Line 
1import os
2import sys
3
4import sas.qtgui.Utilities.GuiUtils as GuiUtils
5from PyQt5 import QtGui, QtCore, QtWidgets
6
7import sas.qtgui.Utilities.ObjectLibrary as ObjectLibrary
8
9from sas.qtgui.Perspectives.Fitting.UI.ConstraintWidgetUI import Ui_ConstraintWidgetUI
10from sas.qtgui.Perspectives.Fitting.FittingWidget import FittingWidget
11from sas.qtgui.Perspectives.Fitting.ComplexConstraint import ComplexConstraint
12from sas.qtgui.Perspectives.Fitting.Constraints import Constraint
13
14class ConstraintWidget(QtWidgets.QWidget, Ui_ConstraintWidgetUI):
15    """
16    Constraints Dialog to select the desired parameter/model constraints.
17    """
18
19    def __init__(self, parent=None):
20        super(ConstraintWidget, self).__init__()
21        self.parent = parent
22        self.setupUi(self)
23        self.currentType = "FitPage"
24
25        # Remember previous content of modified cell
26        self.current_cell = ""
27
28        # Set up the widgets
29        self.initializeWidgets()
30
31        # Set up signals/slots
32        self.initializeSignals()
33
34        # Create the list of tabs
35        self.initializeFitList()
36
37    def acceptsData(self):
38        """ Tells the caller this widget doesn't accept data """
39        return False
40
41    def initializeWidgets(self):
42        """
43        Set up various widget states
44        """
45        labels = ['FitPage', 'Model', 'Data', 'Mnemonic']
46        # tab widget - headers
47        self.editable_tab_columns = [labels.index('Mnemonic')]
48        self.tblTabList.setColumnCount(len(labels))
49        self.tblTabList.setHorizontalHeaderLabels(labels)
50        self.tblTabList.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
51
52        self.tblTabList.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
53        self.tblTabList.customContextMenuRequested.connect(self.showModelContextMenu)
54
55        # disabled constraint
56        labels = ['Constraint']
57        self.tblConstraints.setColumnCount(len(labels))
58        self.tblConstraints.setHorizontalHeaderLabels(labels)
59        self.tblConstraints.horizontalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Stretch)
60        self.tblConstraints.setEnabled(False)
61
62        self.tblConstraints.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
63        self.tblConstraints.customContextMenuRequested.connect(self.showConstrContextMenu)
64
65    def initializeSignals(self):
66        """
67        Set up signals/slots for this widget
68        """
69        # simple widgets
70        self.btnSingle.toggled.connect(self.onFitTypeChange)
71        self.btnBatch.toggled.connect(self.onFitTypeChange)
72        self.cbCases.currentIndexChanged.connect(self.onSpecialCaseChange)
73        self.cmdFit.clicked.connect(self.onFit)
74        self.cmdHelp.clicked.connect(self.onHelp)
75
76        # QTableWidgets
77        self.tblTabList.cellChanged.connect(self.onMonikerEdit)
78        self.tblTabList.cellDoubleClicked.connect(self.onTabCellEntered)
79        self.tblConstraints.cellChanged.connect(self.onConstraintChange)
80
81        # External signals
82        self.parent.tabsModifiedSignal.connect(self.initializeFitList)
83
84    def updateSignalsFromTab(self, tab=None):
85        """
86        Intercept update signals from fitting tabs
87        """
88        if tab is not None:
89            ObjectLibrary.getObject(tab).constraintAddedSignal.connect(self.initializeFitList)
90            ObjectLibrary.getObject(tab).newModelSignal.connect(self.initializeFitList)
91
92    def onFitTypeChange(self, checked):
93        """
94        Respond to the fit type change
95        single fit/batch fit
96        """
97        pass
98
99    def onSpecialCaseChange(self, index):
100        """
101        Respond to the combobox change for special case constraint sets
102        """
103        pass
104
105    def onFit(self):
106        """
107        Perform the constrained/simultaneous fit
108        """
109        pass
110
111    def onHelp(self):
112        """
113        Display the help page
114        """
115        pass
116
117    def onMonikerEdit(self, row, column):
118        """
119        Modify the model moniker
120        """
121        if column not in self.editable_tab_columns:
122            return
123        item = self.tblTabList.item(row, column)
124        new_moniker = item.data(0)
125
126        # The new name should be validated on the fly, with QValidator
127        # but let's just assure it post-factum
128        is_good_moniker = self.validateMoniker(new_moniker)
129        is_good_moniker = True
130        if not is_good_moniker:
131            item.setBackground(QtCore.Qt.red)
132            self.cmdFit.setEnabled(False)
133        else:
134            self.tblTabList.blockSignals(True)
135            item.setBackground(QtCore.Qt.white)
136            self.tblTabList.blockSignals(False)
137            self.cmdFit.setEnabled(True)
138            if not self.current_cell:
139                return
140            # Remember the value
141            if self.current_cell not in self.available_tabs:
142                return
143            temp_tab = self.available_tabs[self.current_cell]
144            # Remove the key from the dictionaries
145            self.available_tabs.pop(self.current_cell, None)
146            # Change the model name
147            model = temp_tab.kernel_module
148            model.name = new_moniker
149            # Replace constraint name
150            temp_tab.replaceConstraintName(self.current_cell, new_moniker)
151            # Reinitialize the display
152            self.initializeFitList()
153        pass
154
155    def onConstraintChange(self, row, column):
156        """
157        Modify the constraint in-place.
158        Tricky.
159        """
160        pass
161
162    def onTabCellEntered(self, row, column):
163        """
164        Remember the original tab list cell data.
165        Needed for reverting back on bad validation
166        """
167        if column != 3:
168            return
169        self.current_cell = self.tblTabList.item(row, column).data(0)
170
171    def isTabImportable(self, tab):
172        """
173        Determines if the tab can be imported and included in the widget
174        """
175        if not self.currentType in tab: return False
176        object = ObjectLibrary.getObject(tab)
177        if not isinstance(object, FittingWidget): return False
178        if object.data is None: return False
179        return True
180
181    def showModelContextMenu(self, position):
182        """
183        Show context specific menu in the tab table widget.
184        """
185        menu = QtWidgets.QMenu()
186        rows = [s.row() for s in self.tblTabList.selectionModel().selectedRows()]
187        num_rows = len(rows)
188        if num_rows <= 0:
189            return
190        # Select for fitting
191        param_string = "Fit Page " if num_rows==1 else "Fit Pages "
192        to_string = "to its current value" if num_rows==1 else "to their current values"
193
194        self.actionSelect = QtWidgets.QAction(self)
195        self.actionSelect.setObjectName("actionSelect")
196        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
197        # Unselect from fitting
198        self.actionDeselect = QtWidgets.QAction(self)
199        self.actionDeselect.setObjectName("actionDeselect")
200        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
201
202        self.actionRemoveConstraint = QtWidgets.QAction(self)
203        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
204        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove all constraints on selected models"))
205
206        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
207        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
208        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of parameters in selected models..."))
209
210        menu.addAction(self.actionSelect)
211        menu.addAction(self.actionDeselect)
212        menu.addSeparator()
213
214        #menu.addAction(self.actionRemoveConstraint)
215        if num_rows >= 2:
216            menu.addAction(self.actionMutualMultiConstrain)
217
218        # Define the callbacks
219        #self.actionConstrain.triggered.connect(self.addSimpleConstraint)
220        #self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
221        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
222        self.actionSelect.triggered.connect(self.selectModels)
223        self.actionDeselect.triggered.connect(self.deselectModels)
224        try:
225            menu.exec_(self.tblTabList.viewport().mapToGlobal(position))
226        except AttributeError as ex:
227            logging.error("Error generating context menu: %s" % ex)
228        return
229
230    def showConstrContextMenu(self, position):
231        """
232        Show context specific menu in the tab table widget.
233        """
234        menu = QtWidgets.QMenu()
235        rows = [s.row() for s in self.tblConstraints.selectionModel().selectedRows()]
236        num_rows = len(rows)
237        if num_rows <= 0:
238            return
239        # Select for fitting
240        param_string = "constraint " if num_rows==1 else "constraints "
241        to_string = "to its current value" if num_rows==1 else "to their current values"
242
243        self.actionSelect = QtWidgets.QAction(self)
244        self.actionSelect.setObjectName("actionSelect")
245        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
246        # Unselect from fitting
247        self.actionDeselect = QtWidgets.QAction(self)
248        self.actionDeselect.setObjectName("actionDeselect")
249        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
250
251        self.actionRemoveConstraint = QtWidgets.QAction(self)
252        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
253        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove "+param_string))
254
255        menu.addAction(self.actionSelect)
256        menu.addAction(self.actionDeselect)
257        menu.addSeparator()
258        menu.addAction(self.actionRemoveConstraint)
259
260        # Define the callbacks
261        #self.actionConstrain.triggered.connect(self.addSimpleConstraint)
262        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
263        #self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
264        self.actionSelect.triggered.connect(self.selectConstraints)
265        self.actionDeselect.triggered.connect(self.deselectConstraints)
266        try:
267            menu.exec_(self.tblConstraints.viewport().mapToGlobal(position))
268        except AttributeError as ex:
269            logging.error("Error generating context menu: %s" % ex)
270        return
271
272    def selectConstraints(self):
273        """
274        Selected constraints are chosen for fitting
275        """
276        status = QtCore.Qt.Checked
277        self.setRowSelection(self.tblConstraints, status)
278
279    def deselectConstraints(self):
280        """
281        Selected constraints are removed for fitting
282        """
283        status = QtCore.Qt.Unchecked
284        self.setRowSelection(self.tblConstraints, status)
285
286    def selectModels(self):
287        """
288        Selected models are chosen for fitting
289        """
290        status = QtCore.Qt.Checked
291        self.setRowSelection(self.tblTabList, status)
292
293    def deselectModels(self):
294        """
295        Selected models are removed for fitting
296        """
297        status = QtCore.Qt.Unchecked
298        self.setRowSelection(self.tblTabList, status)
299
300    def selectedParameters(self, widget):
301        """ Returns list of selected (highlighted) parameters """
302        return [s.row() for s in widget.selectionModel().selectedRows()]
303
304    def setRowSelection(self, widget, status=QtCore.Qt.Unchecked):
305        """
306        Selected models are chosen for fitting
307        """
308        # Convert to proper indices and set requested enablement
309        for row in self.selectedParameters(widget):
310            widget.item(row, 0).setCheckState(status)
311
312    def deleteConstraint(self):#, row):
313        """
314        Delete all selected constraints.
315        """
316        # Removing rows from the table we're iterating over,
317        # so prepare a list of data first
318        constraints_to_delete = []
319        for row in self.selectedParameters(self.tblConstraints):
320            constraints_to_delete.append(self.tblConstraints.item(row, 0).data(0))
321        for constraint in constraints_to_delete:
322            moniker = constraint[:constraint.index(':')]
323            param = constraint[constraint.index(':')+1:constraint.index('=')].strip()
324            tab = self.available_tabs[moniker]
325            tab.deleteConstraintOnParameter(param)
326        # Constraints removed - refresh the table widget
327        self.initializeFitList()
328
329    def uneditableItem(self, data=""):
330        """
331        Returns an uneditable Table Widget Item
332        """
333        item = QtWidgets.QTableWidgetItem(data)
334        item.setFlags( QtCore.Qt.ItemIsSelectable |  QtCore.Qt.ItemIsEnabled )
335        return item
336
337    def updateFitLine(self, tab):
338        """
339        Update a single line of the table widget with tab info
340        """
341        model = ObjectLibrary.getObject(tab).kernel_module
342        if model is None:
343            return
344        tab_name = tab
345        model_name = model.id
346        moniker = model.name
347        model_data = ObjectLibrary.getObject(tab).data
348        model_filename = model_data.filename
349        self.available_tabs[moniker] = ObjectLibrary.getObject(tab)
350
351        # Update the model table widget
352        #item = QtWidgets.QTableWidgetItem(tab_name)
353        #item.setCheckState(QtCore.Qt.Checked)
354        #item.setFlags( QtCore.Qt.ItemIsSelectable |  QtCore.Qt.ItemIsEnabled )
355        pos = self.tblTabList.rowCount()
356        self.tblTabList.insertRow(pos)
357        item = self.uneditableItem(tab_name)
358        item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
359        item.setCheckState(QtCore.Qt.Checked)
360        self.tblTabList.setItem(pos, 0, item)
361        self.tblTabList.setItem(pos, 1, self.uneditableItem(model_name))
362        self.tblTabList.setItem(pos, 2, self.uneditableItem(model_filename))
363        # Moniker is editable, so no option change
364        item = QtWidgets.QTableWidgetItem(moniker)
365        # Disable signals so we don't get infinite call recursion
366        self.tblTabList.blockSignals(True)
367        self.tblTabList.setItem(pos, 3, item)
368        self.tblTabList.blockSignals(False)
369
370        # Check if any constraints present in tab
371        constraints = ObjectLibrary.getObject(tab).getConstraintsForModel()
372        if not constraints: 
373            return
374        self.tblConstraints.setEnabled(True)
375        for constraint in constraints:
376            # Create the text for widget item
377            label = moniker + ":"+ constraint[0] + " = " + constraint[1]
378
379            # Show the text in the constraint table
380            item = QtWidgets.QTableWidgetItem(label)
381            item.setCheckState(QtCore.Qt.Checked)
382            pos = self.tblConstraints.rowCount()
383            self.tblConstraints.insertRow(pos)
384            self.tblConstraints.setItem(pos, 0, item)
385            self.available_constraints[pos] = constraints
386
387    def initializeFitList(self):
388        """
389        Fill the list of model/data sets for fitting/constraining
390        """
391        # look at the object library to find all fit tabs
392        # Show the content of the current "model"
393        objects = ObjectLibrary.listObjects()
394
395        # Tab dict
396        # moniker -> (kernel_module, data)
397        self.available_tabs = {}
398        # Constraint dict
399        # moniker -> [constraints]
400        self.available_constraints = {}
401
402        # Reset the table widgets
403        self.tblTabList.setRowCount(0)
404        self.tblConstraints.setRowCount(0)
405
406        # Fit disabled
407        self.cmdFit.setEnabled(False)
408
409        if not objects:
410            return
411
412        tabs = [tab for tab in ObjectLibrary.listObjects() if self.isTabImportable(tab)]
413        for tab in tabs:
414            self.updateFitLine(tab)
415            self.updateSignalsFromTab(tab)
416            # We have at least 1 fit page, allow fitting
417            self.cmdFit.setEnabled(True)
418
419    def validateMoniker(self, new_moniker=None):
420        """
421        Check new_moniker for correctness.
422        It must be non-empty.
423        It must not be the same as other monikers.
424        """
425        if not new_moniker:
426            return False
427
428        for existing_moniker in self.available_tabs:
429            if existing_moniker == new_moniker and existing_moniker != self.current_cell:
430                return False
431
432        return True
433
434    def getObjectByName(self, name):
435        for object_name in ObjectLibrary.listObjects():
436            object = ObjectLibrary.getObject(object_name)
437            if isinstance(object, FittingWidget):
438                try:
439                    if object.kernel_module.name == name:
440                        return object
441                except AttributeError:
442                    # Disregard atribute errors - empty fit widgets
443                    continue
444        return None
445
446    def showMultiConstraint(self):
447        """
448        Invoke the complex constraint editor
449        """
450        selected_rows = self.selectedParameters(self.tblTabList)
451        assert(len(selected_rows)==2)
452
453        tab_list = [ObjectLibrary.getObject(self.tblTabList.item(s, 0).data(0)) for s in selected_rows]
454        # Create and display the widget for param1 and param2
455        cc_widget = ComplexConstraint(self, tabs=tab_list)
456        if cc_widget.exec_() != QtWidgets.QDialog.Accepted:
457            return
458
459        constraint = Constraint()
460        model1, param1, operator, constraint_text = cc_widget.constraint()
461
462        constraint.func = constraint_text
463        # constraint.param = param1
464        # Find the right tab
465        constrained_tab = self.getObjectByName(model1)
466        if constrained_tab is None:
467            return
468
469        # Find the constrained parameter row
470        constrained_row = constrained_tab.getRowFromName(param1)
471
472        # Update the tab
473        constrained_tab.addConstraintToRow(constraint, constrained_row)
474        pass
Note: See TracBrowser for help on using the repository browser.