source: sasview/src/sas/qtgui/Perspectives/Fitting/ComplexConstraint.py @ 9c207f5

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 9c207f5 was 9c207f5, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 5 years ago

Fixed onSetAll behaviour

  • Property mode set to 100644
File size: 11.0 KB
RevLine 
[2d466e4]1"""
2Widget for multi-model constraints.
3"""
4import os
[725d9c06]5
6# numpy methods required for the validator! Don't remove.
7# pylint: disable=unused-import,unused-wildcard-import,redefined-builtin
[2d466e4]8from numpy import *
9
10from PyQt5 import QtCore
11from PyQt5 import QtGui
12from PyQt5 import QtWidgets
13import webbrowser
14
[305114c]15from sas.qtgui.Perspectives.Fitting import FittingUtilities
[2d466e4]16import sas.qtgui.Utilities.GuiUtils as GuiUtils
[ecc5d043]17from sas.qtgui.Perspectives.Fitting.Constraint import Constraint
18
[d72ac57]19#ALLOWED_OPERATORS = ['=','<','>','>=','<=']
20ALLOWED_OPERATORS = ['=']
[2d466e4]21
22# Local UI
23from sas.qtgui.Perspectives.Fitting.UI.ComplexConstraintUI import Ui_ComplexConstraintUI
24
25class ComplexConstraint(QtWidgets.QDialog, Ui_ComplexConstraintUI):
[ecc5d043]26    constraintReadySignal = QtCore.pyqtSignal(tuple)
[2d466e4]27    def __init__(self, parent=None, tabs=None):
28        super(ComplexConstraint, self).__init__()
29
30        self.setupUi(self)
31        self.setModal(True)
32
33        # Useful globals
34        self.tabs = tabs
35        self.params = None
36        self.tab_names = None
37        self.operator = '='
[ecc5d043]38        self._constraint = Constraint()
[2e5081b]39        self.all_menu   = None
[2d466e4]40
[305114c]41        self.warning = self.lblWarning.text()
[2d466e4]42        self.setupData()
43        self.setupSignals()
[305114c]44        self.setupWidgets()
[da9a0722]45        self.setupTooltip()
[2d466e4]46
[14ec91c5]47        # Default focus is on OK
48        self.cmdOK.setFocus()
49
[2d466e4]50    def setupData(self):
51        """
52        Digs into self.tabs and pulls out relevant info
53        """
54        self.tab_names = [tab.kernel_module.name for tab in self.tabs]
55        self.params = [tab.getParamNames() for tab in self.tabs]
56
57    def setupSignals(self):
58        """
59        Signals from various elements
60        """
[ecc5d043]61        self.cmdOK.clicked.connect(self.onApply)
[2d466e4]62        self.cmdHelp.clicked.connect(self.onHelp)
[c5a2722f]63        self.txtConstraint.editingFinished.connect(self.validateFormula)
[2e5081b]64        self.cbModel1.currentIndexChanged.connect(self.onModelIndexChange)
65        self.cbModel2.currentIndexChanged.connect(self.onModelIndexChange)
[2d466e4]66
67        self.cbParam1.currentIndexChanged.connect(self.onParamIndexChange)
68        self.cbParam2.currentIndexChanged.connect(self.onParamIndexChange)
69        self.cbOperator.currentIndexChanged.connect(self.onOperatorChange)
70
71    def setupWidgets(self):
72        """
73        Setup widgets based on current parameters
74        """
[2e5081b]75        self.cbModel1.insertItems(0, self.tab_names)
76        self.cbModel2.insertItems(0, self.tab_names)
[2d466e4]77
[ecc5d043]78        self.setupParamWidgets()
79
[2e5081b]80        self.setupMenu()
81
82    def setupMenu(self):
83        # Add menu to the Apply button, if necessary
84        if self.cbModel1.currentText() ==self.cbModel2.currentText():
85            self.cmdOK.setArrowType(QtCore.Qt.NoArrow)
86            self.cmdOK.setPopupMode(QtWidgets.QToolButton.DelayedPopup)
87            self.cmdOK.setMenu(None)
88            return
89        self.all_menu   = QtWidgets.QMenu()
[ecc5d043]90        self.actionAddAll = QtWidgets.QAction(self)
91        self.actionAddAll.setObjectName("actionAddAll")
92        self.actionAddAll.setText(QtCore.QCoreApplication.translate("self", "Add all"))
93        ttip = "Add constraints between all identically named parameters in both fitpages"
94        self.actionAddAll.setToolTip(ttip)
95        self.actionAddAll.triggered.connect(self.onSetAll)
[2e5081b]96        self.all_menu.addAction(self.actionAddAll)
[ecc5d043]97        # https://bugreports.qt.io/browse/QTBUG-13663
[2e5081b]98        self.all_menu.setToolTipsVisible(True)
99        self.cmdOK.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
100        self.cmdOK.setArrowType(QtCore.Qt.DownArrow)
101        self.cmdOK.setMenu(self.all_menu)
[ecc5d043]102
103    def setupParamWidgets(self):
104        """
105        Fill out comboboxes and set labels with non-constrained parameters
106        """
[2d466e4]107        self.cbParam1.clear()
[2e5081b]108        tab_index1 = self.cbModel1.currentIndex()
109        items1 = [param for param in self.params[tab_index1] if not self.tabs[tab_index1].paramHasConstraint(param)]
[ecc5d043]110        self.cbParam1.addItems(items1)
111
112        # M2 doesn't have to be non-constrained
[2d466e4]113        self.cbParam2.clear()
[2e5081b]114        tab_index2 = self.cbModel2.currentIndex()
115        items2 = [param for param in self.params[tab_index2]]
[ecc5d043]116        self.cbParam2.addItems(items2)
[2d466e4]117
[2e5081b]118        self.txtParam.setText(self.tab_names[tab_index1] + ":" + self.cbParam1.currentText())
[2d466e4]119
120        self.cbOperator.clear()
121        self.cbOperator.addItems(ALLOWED_OPERATORS)
122        self.txtOperator.setText(self.cbOperator.currentText())
123
[2e5081b]124        self.txtConstraint.setText(self.tab_names[tab_index2]+"."+self.cbParam2.currentText())
[2d466e4]125
[ecc5d043]126        # disable Apply if no parameters available
127        if len(items1)==0:
128            self.cmdOK.setEnabled(False)
129            txt = "No parameters in model "+self.tab_names[0] +\
130                " are available for constraining."
131            self.lblWarning.setText(txt)
132        else:
133            self.cmdOK.setEnabled(True)
134            txt = ""
135            self.lblWarning.setText(txt)
136
[2d466e4]137    def setupTooltip(self):
138        """
139        Tooltip for txtConstraint
140        """
[da9a0722]141        p1 = self.tab_names[0] + ":" + self.cbParam1.currentText()
142        p2 = self.tab_names[1]+"."+self.cbParam2.currentText()
143        tooltip = "E.g.\n%s = 2.0 * (%s)\n" %(p1, p2)
144        tooltip += "%s = sqrt(%s) + 5"%(p1, p2)
[2d466e4]145        self.txtConstraint.setToolTip(tooltip)
146
147    def onParamIndexChange(self, index):
148        """
149        Respond to parameter combo box changes
150        """
151        # Find out the signal source
152        source = self.sender().objectName()
[305114c]153        param1 = self.cbParam1.currentText()
154        param2 = self.cbParam2.currentText()
[2d466e4]155        if source == "cbParam1":
[305114c]156            self.txtParam.setText(self.tab_names[0] + ":" + param1)
[2d466e4]157        else:
[305114c]158            self.txtConstraint.setText(self.tab_names[1] + "." + param2)
159        # Check if any of the parameters are polydisperse
160        params_list = [param1, param2]
161        all_pars = [tab.model_parameters for tab in self.tabs]
162        is2Ds = [tab.is2D for tab in self.tabs]
163        txt = ""
164        for pars, is2D in zip(all_pars, is2Ds):
165            if any([FittingUtilities.isParamPolydisperse(p, pars, is2D) for p in params_list]):
166                # no parameters are pd - reset the text to not show the warning
167                txt = self.warning
168        self.lblWarning.setText(txt)
169
[2e5081b]170    def onModelIndexChange(self, index):
171        """
172        Respond to mode combo box changes
173        """
174        # disable/enable Add All
175        self.setupMenu()
176        # Reload parameters
177        self.setupParamWidgets()
[2d466e4]178
179    def onOperatorChange(self, index):
180        """
181        Respond to operator combo box changes
182        """
183        self.txtOperator.setText(self.cbOperator.currentText())
184
185    def validateFormula(self):
186        """
187        Add visual cues when formula is incorrect
188        """
[2e5081b]189        # temporarily disable validation
190        return
191        #
[2d466e4]192        formula_is_valid = self.validateConstraint(self.txtConstraint.text())
193        if not formula_is_valid:
194            self.cmdOK.setEnabled(False)
195            self.txtConstraint.setStyleSheet("QLineEdit {background-color: red;}")
196        else:
197            self.cmdOK.setEnabled(True)
198            self.txtConstraint.setStyleSheet("QLineEdit {background-color: white;}")
199
200    def validateConstraint(self, constraint_text):
201        """
202        Ensure the constraint has proper form
203        """
204        # 0. none or empty
205        if not constraint_text or not isinstance(constraint_text, str):
206            return False
207
[2e5081b]208        # M1.scale --> model_str='M1', constraint_text='scale'
[c5a2722f]209        param_str = self.cbParam2.currentText()
[2d466e4]210        constraint_text = constraint_text.strip()
[2e5081b]211        model_str = self.cbModel2.currentText()
[c5a2722f]212
213        # 0. Has to contain the model name
[2e5081b]214        if model_str != model_str:
[c5a2722f]215            return False
216
[ba01ad1]217        # Remove model name from constraint
[c5a2722f]218        constraint_text = constraint_text.replace(model_str+".",'')
[2d466e4]219
220        # 1. just the parameter
221        if param_str == constraint_text:
222            return True
223
224        # 2. ensure the text contains parameter name
225        parameter_string_start = constraint_text.find(param_str)
226        if parameter_string_start < 0:
227            return False
228
[c5a2722f]229        # 3. replace parameter name with "1" and try to evaluate the expression
[2d466e4]230        try:
231            expression_to_evaluate = constraint_text.replace(param_str, "1.0")
232            eval(expression_to_evaluate)
233        except Exception:
234            # Too many cases to cover individually, just a blanket
235            # Exception should be sufficient
236            # Note that in current numpy things like sqrt(-1) don't
237            # raise but just return warnings
238            return False
239
240        return True
241
[c5a2722f]242    def constraint(self):
243        """
[ecc5d043]244        Return the generated constraint
245        """
246        param = self.cbParam1.currentText()
247        value = self.cbParam2.currentText()
248        func = self.txtConstraint.text()
[2e5081b]249        value_ex = self.cbModel2.currentText() + "." + self.cbParam2.currentText()
250        model1 = self.cbModel1.currentText()
[ecc5d043]251        operator = self.cbOperator.currentText()
252
253        con = Constraint(self,
254                         param=param,
255                         value=value,
256                         func=func,
257                         value_ex=value_ex,
258                         operator=operator)
259
260        return (model1, con)
261
262    def onApply(self):
263        """
264        Respond to Add constraint action.
265        Send a signal that the constraint is ready to be applied
266        """
267        cons_tuple = self.constraint()
268        self.constraintReadySignal.emit(cons_tuple)
269        # reload the comboboxes
270        self.setupParamWidgets()
271
272    def onSetAll(self):
273        """
274        Set constraints on all identically named parameters between two fitpages
[c5a2722f]275        """
[ecc5d043]276        # loop over parameters in constrained model
[9c207f5]277        index1 = self.cbModel1.currentIndex()
278        index2 = self.cbModel2.currentIndex()
279        items1 = [param for param in self.params[index1] if not self.tabs[index1].paramHasConstraint(param)]
280        items2 = self.params[index2]
[ecc5d043]281        for item in items1:
282            if item not in items2: continue
283            param = item
284            value = item
[2e5081b]285            func = self.cbModel2.currentText() + "." + param
286            value_ex = self.cbModel1.currentText() + "." + param
287            model1 = self.cbModel1.currentText()
[ecc5d043]288            operator = self.cbOperator.currentText()
289
290            con = Constraint(self,
291                             param=param,
292                             value=value,
293                             func=func,
294                             value_ex=value_ex,
295                             operator=operator)
296
297            self.constraintReadySignal.emit((model1, con))
298
299        # reload the comboboxes
300        self.setupParamWidgets()
[c5a2722f]301
[2d466e4]302    def onHelp(self):
303        """
304        Display related help section
305        """
306        try:
307            help_location = GuiUtils.HELP_DIRECTORY_LOCATION + \
[aed0532]308            "/user/qtgui/Perspectives/Fitting/fitting_help.html#simultaneous-fits-with-constraints"
[2d466e4]309            webbrowser.open('file://' + os.path.realpath(help_location))
310        except AttributeError:
311            # No manager defined - testing and standalone runs
312            pass
313
314
315
Note: See TracBrowser for help on using the repository browser.