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

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 0764593 was 0764593, checked in by Piotr Rozyczko <rozyczko@…>, 6 years ago

Minor fixes to the complex constraint widget

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