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

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

Reworked the complex constraint functionality SASVIEW-1019

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