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

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

Added batch fit constraints.
Cleaned up interactions between constraints in various tabs

  • Property mode set to 100644
File size: 19.4 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        is_good_moniker = True
144        if not is_good_moniker:
145            item.setBackground(QtCore.Qt.red)
146            self.cmdFit.setEnabled(False)
147            return
148        self.tblTabList.blockSignals(True)
149        item.setBackground(QtCore.Qt.white)
150        self.tblTabList.blockSignals(False)
151        self.cmdFit.setEnabled(True)
152        if not self.current_cell:
153            return
154        # Remember the value
155        if self.current_cell not in self.available_tabs:
156            return
157        temp_tab = self.available_tabs[self.current_cell]
158        # Remove the key from the dictionaries
159        self.available_tabs.pop(self.current_cell, None)
160        # Change the model name
161        model = temp_tab.kernel_module
162        model.name = new_moniker
163        # Replace constraint name
164        temp_tab.replaceConstraintName(self.current_cell, new_moniker)
165        # Reinitialize the display
166        self.initializeFitList()
167
168    def onConstraintChange(self, row, column):
169        """
170        Modify the constraint in-place.
171        """
172        item = self.tblConstraints.item(row, column)
173        if column == 0:
174            # Update the tabs for fitting list
175            constraint = self.available_constraints[row]
176            constraint.active = (item.checkState() == QtCore.Qt.Checked)
177
178    def onTabCellEntered(self, row, column):
179        """
180        Remember the original tab list cell data.
181        Needed for reverting back on bad validation
182        """
183        if column != 3:
184            return
185        self.current_cell = self.tblTabList.item(row, column).data(0)
186
187    def onModifiedTabs(self):
188        """
189        Respond to tabs being deleted by deleting involved constraints
190
191        This should probably be done in FittingWidget as it is the owner of
192        all the fitting data, but I want to keep the FW oblivious about
193        dependence on other FW tabs, so enforcing the constraint deletion here.
194        """
195        # Get the list of all constraints from querying the table
196        #constraints = getConstraintsForModel()
197
198        # Get the current list of tabs
199        #tabs = ObjectLibrary.listObjects()
200
201        # Check if any of the constraint dependencies got deleted
202        # Check the list of constraints
203        self.initializeFitList()
204        pass
205
206    def isTabImportable(self, tab):
207        """
208        Determines if the tab can be imported and included in the widget
209        """
210        if not self.currentType in tab: return False
211        object = ObjectLibrary.getObject(tab)
212        if not isinstance(object, FittingWidget): return False
213        if object.data is None: return False
214        return True
215
216    def showModelContextMenu(self, position):
217        """
218        Show context specific menu in the tab table widget.
219        """
220        menu = QtWidgets.QMenu()
221        rows = [s.row() for s in self.tblTabList.selectionModel().selectedRows()]
222        num_rows = len(rows)
223        if num_rows <= 0:
224            return
225        # Select for fitting
226        param_string = "Fit Page " if num_rows==1 else "Fit Pages "
227        to_string = "to its current value" if num_rows==1 else "to their current values"
228
229        self.actionSelect = QtWidgets.QAction(self)
230        self.actionSelect.setObjectName("actionSelect")
231        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
232        # Unselect from fitting
233        self.actionDeselect = QtWidgets.QAction(self)
234        self.actionDeselect.setObjectName("actionDeselect")
235        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
236
237        self.actionRemoveConstraint = QtWidgets.QAction(self)
238        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
239        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove all constraints on selected models"))
240
241        self.actionMutualMultiConstrain = QtWidgets.QAction(self)
242        self.actionMutualMultiConstrain.setObjectName("actionMutualMultiConstrain")
243        self.actionMutualMultiConstrain.setText(QtCore.QCoreApplication.translate("self", "Mutual constrain of parameters in selected models..."))
244
245        menu.addAction(self.actionSelect)
246        menu.addAction(self.actionDeselect)
247        menu.addSeparator()
248
249        #menu.addAction(self.actionRemoveConstraint)
250        if num_rows >= 2:
251            menu.addAction(self.actionMutualMultiConstrain)
252
253        # Define the callbacks
254        #self.actionConstrain.triggered.connect(self.addSimpleConstraint)
255        #self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
256        self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
257        self.actionSelect.triggered.connect(self.selectModels)
258        self.actionDeselect.triggered.connect(self.deselectModels)
259        try:
260            menu.exec_(self.tblTabList.viewport().mapToGlobal(position))
261        except AttributeError as ex:
262            logging.error("Error generating context menu: %s" % ex)
263        return
264
265    def showConstrContextMenu(self, position):
266        """
267        Show context specific menu in the tab table widget.
268        """
269        menu = QtWidgets.QMenu()
270        rows = [s.row() for s in self.tblConstraints.selectionModel().selectedRows()]
271        num_rows = len(rows)
272        if num_rows <= 0:
273            return
274        # Select for fitting
275        param_string = "constraint " if num_rows==1 else "constraints "
276        to_string = "to its current value" if num_rows==1 else "to their current values"
277
278        self.actionSelect = QtWidgets.QAction(self)
279        self.actionSelect.setObjectName("actionSelect")
280        self.actionSelect.setText(QtCore.QCoreApplication.translate("self", "Select "+param_string+" for fitting"))
281        # Unselect from fitting
282        self.actionDeselect = QtWidgets.QAction(self)
283        self.actionDeselect.setObjectName("actionDeselect")
284        self.actionDeselect.setText(QtCore.QCoreApplication.translate("self", "De-select "+param_string+" from fitting"))
285
286        self.actionRemoveConstraint = QtWidgets.QAction(self)
287        self.actionRemoveConstraint.setObjectName("actionRemoveConstrain")
288        self.actionRemoveConstraint.setText(QtCore.QCoreApplication.translate("self", "Remove "+param_string))
289
290        menu.addAction(self.actionSelect)
291        menu.addAction(self.actionDeselect)
292        menu.addSeparator()
293        menu.addAction(self.actionRemoveConstraint)
294
295        # Define the callbacks
296        #self.actionConstrain.triggered.connect(self.addSimpleConstraint)
297        self.actionRemoveConstraint.triggered.connect(self.deleteConstraint)
298        #self.actionMutualMultiConstrain.triggered.connect(self.showMultiConstraint)
299        self.actionSelect.triggered.connect(self.selectConstraints)
300        self.actionDeselect.triggered.connect(self.deselectConstraints)
301        try:
302            menu.exec_(self.tblConstraints.viewport().mapToGlobal(position))
303        except AttributeError as ex:
304            logging.error("Error generating context menu: %s" % ex)
305        return
306
307    def selectConstraints(self):
308        """
309        Selected constraints are chosen for fitting
310        """
311        status = QtCore.Qt.Checked
312        self.setRowSelection(self.tblConstraints, status)
313
314    def deselectConstraints(self):
315        """
316        Selected constraints are removed for fitting
317        """
318        status = QtCore.Qt.Unchecked
319        self.setRowSelection(self.tblConstraints, status)
320
321    def selectModels(self):
322        """
323        Selected models are chosen for fitting
324        """
325        status = QtCore.Qt.Checked
326        self.setRowSelection(self.tblTabList, status)
327
328    def deselectModels(self):
329        """
330        Selected models are removed for fitting
331        """
332        status = QtCore.Qt.Unchecked
333        self.setRowSelection(self.tblTabList, status)
334
335    def selectedParameters(self, widget):
336        """ Returns list of selected (highlighted) parameters """
337        return [s.row() for s in widget.selectionModel().selectedRows()]
338
339    def setRowSelection(self, widget, status=QtCore.Qt.Unchecked):
340        """
341        Selected models are chosen for fitting
342        """
343        # Convert to proper indices and set requested enablement
344        for row in self.selectedParameters(widget):
345            widget.item(row, 0).setCheckState(status)
346
347    def deleteConstraint(self):#, row):
348        """
349        Delete all selected constraints.
350        """
351        # Removing rows from the table we're iterating over,
352        # so prepare a list of data first
353        constraints_to_delete = []
354        for row in self.selectedParameters(self.tblConstraints):
355            constraints_to_delete.append(self.tblConstraints.item(row, 0).data(0))
356        for constraint in constraints_to_delete:
357            moniker = constraint[:constraint.index(':')]
358            param = constraint[constraint.index(':')+1:constraint.index('=')].strip()
359            tab = self.available_tabs[moniker]
360            tab.deleteConstraintOnParameter(param)
361        # Constraints removed - refresh the table widget
362        self.initializeFitList()
363
364    def uneditableItem(self, data=""):
365        """
366        Returns an uneditable Table Widget Item
367        """
368        item = QtWidgets.QTableWidgetItem(data)
369        item.setFlags( QtCore.Qt.ItemIsSelectable |  QtCore.Qt.ItemIsEnabled )
370        return item
371
372    def updateFitLine(self, tab):
373        """
374        Update a single line of the table widget with tab info
375        """
376        fit_page = ObjectLibrary.getObject(tab)
377        model = fit_page.kernel_module
378        if model is None:
379            return
380        tab_name = tab
381        model_name = model.id
382        moniker = model.name
383        model_data = fit_page.data
384        model_filename = model_data.filename
385        self.available_tabs[moniker] = fit_page
386
387        # Update the model table widget
388        pos = self.tblTabList.rowCount()
389        self.tblTabList.insertRow(pos)
390        item = self.uneditableItem(tab_name)
391        item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
392        if tab_name in self.tabs_for_fitting:
393            state = QtCore.Qt.Checked if self.tabs_for_fitting[tab_name] else QtCore.Qt.Unchecked
394            item.setCheckState(state)
395        else:
396            item.setCheckState(QtCore.Qt.Checked)
397            self.tabs_for_fitting[tab_name] = True
398
399        self.tblTabList.setItem(pos, 0, item)
400        self.tblTabList.setItem(pos, 1, self.uneditableItem(model_name))
401        self.tblTabList.setItem(pos, 2, self.uneditableItem(model_filename))
402        # Moniker is editable, so no option change
403        item = QtWidgets.QTableWidgetItem(moniker)
404        # Disable signals so we don't get infinite call recursion
405        self.tblTabList.blockSignals(True)
406        self.tblTabList.setItem(pos, 3, item)
407        self.tblTabList.blockSignals(False)
408
409        # Check if any constraints present in tab
410        constraint_names = fit_page.getConstraintsForModel()
411        constraints = fit_page.getConstraintObjectsForModel()
412        if not constraints: 
413            return
414        self.tblConstraints.setEnabled(True)
415        for constraint, constraint_name in zip(constraints, constraint_names):
416            # Create the text for widget item
417            label = moniker + ":"+ constraint_name[0] + " = " + constraint_name[1]
418            pos = self.tblConstraints.rowCount()
419            self.available_constraints[pos] = constraint
420
421            # Show the text in the constraint table
422            item = self.uneditableItem(label)
423            item.setFlags(item.flags() ^ QtCore.Qt.ItemIsUserCheckable)
424            item.setCheckState(QtCore.Qt.Checked)
425            self.tblConstraints.insertRow(pos)
426            self.tblConstraints.setItem(pos, 0, item)
427
428    def initializeFitList(self):
429        """
430        Fill the list of model/data sets for fitting/constraining
431        """
432        # look at the object library to find all fit tabs
433        # Show the content of the current "model"
434        objects = ObjectLibrary.listObjects()
435
436        # Tab dict
437        # moniker -> (kernel_module, data)
438        self.available_tabs = {}
439        # Constraint dict
440        # moniker -> [constraints]
441        self.available_constraints = {}
442
443        # Reset the table widgets
444        self.tblTabList.setRowCount(0)
445        self.tblConstraints.setRowCount(0)
446
447        # Fit disabled
448        self.cmdFit.setEnabled(False)
449
450        if not objects:
451            return
452
453        tabs = [tab for tab in ObjectLibrary.listObjects() if self.isTabImportable(tab)]
454        for tab in tabs:
455            self.updateFitLine(tab)
456            self.updateSignalsFromTab(tab)
457            # We have at least 1 fit page, allow fitting
458            self.cmdFit.setEnabled(True)
459
460    def validateMoniker(self, new_moniker=None):
461        """
462        Check new_moniker for correctness.
463        It must be non-empty.
464        It must not be the same as other monikers.
465        """
466        if not new_moniker:
467            return False
468
469        for existing_moniker in self.available_tabs:
470            if existing_moniker == new_moniker and existing_moniker != self.current_cell:
471                return False
472
473        return True
474
475    def getObjectByName(self, name):
476        for object_name in ObjectLibrary.listObjects():
477            object = ObjectLibrary.getObject(object_name)
478            if isinstance(object, FittingWidget):
479                try:
480                    if object.kernel_module.name == name:
481                        return object
482                except AttributeError:
483                    # Disregard atribute errors - empty fit widgets
484                    continue
485        return None
486
487    def showMultiConstraint(self):
488        """
489        Invoke the complex constraint editor
490        """
491        selected_rows = self.selectedParameters(self.tblTabList)
492        assert(len(selected_rows)==2)
493
494        tab_list = [ObjectLibrary.getObject(self.tblTabList.item(s, 0).data(0)) for s in selected_rows]
495        # Create and display the widget for param1 and param2
496        cc_widget = ComplexConstraint(self, tabs=tab_list)
497        if cc_widget.exec_() != QtWidgets.QDialog.Accepted:
498            return
499
500        constraint = Constraint()
501        model1, param1, operator, constraint_text = cc_widget.constraint()
502
503        constraint.func = constraint_text
504        constraint.param = param1
505        # Find the right tab
506        constrained_tab = self.getObjectByName(model1)
507        if constrained_tab is None:
508            return
509
510        # Find the constrained parameter row
511        constrained_row = constrained_tab.getRowFromName(param1)
512
513        # Update the tab
514        constrained_tab.addConstraintToRow(constraint, constrained_row)
515        pass
Note: See TracBrowser for help on using the repository browser.