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

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

Added tooltip on COnstraints table.
Added "validate" parameter to Constraint, allowing for looser validation
of complex, multi-fitpage setups.

  • 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.triggered.connect(self.onSetAll)
85        all_menu.addAction(self.actionAddAll)
86        # https://bugreports.qt.io/browse/QTBUG-13663
87        all_menu.setToolTipsVisible(True)
88        self.cmdOK.setMenu(all_menu)
89
90    def setupParamWidgets(self):
91        """
92        Fill out comboboxes and set labels with non-constrained parameters
93        """
94        self.cbParam1.clear()
95        items1 = [param for param in self.params[0] if not self.tabs[0].paramHasConstraint(param)]
96        self.cbParam1.addItems(items1)
97
98        # M2 doesn't have to be non-constrained
99        self.cbParam2.clear()
100        #items2 = [param for param in self.params[1] if not self.tabs[1].paramHasConstraint(param)]
101        items2 = [param for param in self.params[1]]
102        self.cbParam2.addItems(items2)
103
104        self.txtParam.setText(self.tab_names[0] + ":" + self.cbParam1.currentText())
105
106        self.cbOperator.clear()
107        self.cbOperator.addItems(ALLOWED_OPERATORS)
108        self.txtOperator.setText(self.cbOperator.currentText())
109
110        self.txtConstraint.setText(self.tab_names[1]+"."+self.cbParam2.currentText())
111
112        # disable Apply if no parameters available
113        if len(items1)==0:
114            self.cmdOK.setEnabled(False)
115            txt = "No parameters in model "+self.tab_names[0] +\
116                " are available for constraining."
117            self.lblWarning.setText(txt)
118        else:
119            self.cmdOK.setEnabled(True)
120            txt = ""
121            self.lblWarning.setText(txt)
122
123    def setupTooltip(self):
124        """
125        Tooltip for txtConstraint
126        """
127        p1 = self.tab_names[0] + ":" + self.cbParam1.currentText()
128        p2 = self.tab_names[1]+"."+self.cbParam2.currentText()
129        tooltip = "E.g.\n%s = 2.0 * (%s)\n" %(p1, p2)
130        tooltip += "%s = sqrt(%s) + 5"%(p1, p2)
131        self.txtConstraint.setToolTip(tooltip)
132
133    def onParamIndexChange(self, index):
134        """
135        Respond to parameter combo box changes
136        """
137        # Find out the signal source
138        source = self.sender().objectName()
139        param1 = self.cbParam1.currentText()
140        param2 = self.cbParam2.currentText()
141        if source == "cbParam1":
142            self.txtParam.setText(self.tab_names[0] + ":" + param1)
143        else:
144            self.txtConstraint.setText(self.tab_names[1] + "." + param2)
145        # Check if any of the parameters are polydisperse
146        params_list = [param1, param2]
147        all_pars = [tab.model_parameters for tab in self.tabs]
148        is2Ds = [tab.is2D for tab in self.tabs]
149        txt = ""
150        for pars, is2D in zip(all_pars, is2Ds):
151            if any([FittingUtilities.isParamPolydisperse(p, pars, is2D) for p in params_list]):
152                # no parameters are pd - reset the text to not show the warning
153                txt = self.warning
154        self.lblWarning.setText(txt)
155
156
157    def onOperatorChange(self, index):
158        """
159        Respond to operator combo box changes
160        """
161        self.txtOperator.setText(self.cbOperator.currentText())
162
163    def onRevert(self):
164        """
165        switch M1 <-> M2
166        """
167        # Switch parameters
168        self.params[1], self.params[0] = self.params[0], self.params[1]
169        self.tab_names[1], self.tab_names[0] = self.tab_names[0], self.tab_names[1]
170        self.tabs[1], self.tabs[0] = self.tabs[0], self.tabs[1]
171        # Try to swap parameter names in the line edit
172        current_text = self.txtConstraint.text()
173        new_text = current_text.replace(self.cbParam1.currentText(), self.cbParam2.currentText())
174        self.txtConstraint.setText(new_text)
175        # Update labels and tooltips
176        index1 = self.cbParam1.currentIndex()
177        index2 = self.cbParam2.currentIndex()
178        indexOp = self.cbOperator.currentIndex()
179        self.setupWidgets()
180
181        # Original indices
182        index2 = index2 if index2 >= 0 else 0
183        index1 = index1 if index1 >= 0 else 0
184        self.cbParam1.setCurrentIndex(index2)
185        self.cbParam2.setCurrentIndex(index1)
186        self.cbOperator.setCurrentIndex(indexOp)
187        self.setupTooltip()
188
189    def validateFormula(self):
190        """
191        Add visual cues when formula is incorrect
192        """
193        formula_is_valid = self.validateConstraint(self.txtConstraint.text())
194        if not formula_is_valid:
195            self.cmdOK.setEnabled(False)
196            self.txtConstraint.setStyleSheet("QLineEdit {background-color: red;}")
197        else:
198            self.cmdOK.setEnabled(True)
199            self.txtConstraint.setStyleSheet("QLineEdit {background-color: white;}")
200
201    def validateConstraint(self, constraint_text):
202        """
203        Ensure the constraint has proper form
204        """
205        # 0. none or empty
206        if not constraint_text or not isinstance(constraint_text, str):
207            return False
208
209        # M1.scale  --> model_str='M1', constraint_text='scale'
210        param_str = self.cbParam2.currentText()
211        constraint_text = constraint_text.strip()
212        model_str = self.txtName2.text()
213
214        # 0. Has to contain the model name
215        if model_str != self.txtName2.text():
216            return False
217
218        # Remove model name from constraint
219        constraint_text = constraint_text.replace(model_str+".",'')
220
221        # 1. just the parameter
222        if param_str == constraint_text:
223            return True
224
225        # 2. ensure the text contains parameter name
226        parameter_string_start = constraint_text.find(param_str)
227        if parameter_string_start < 0:
228            return False
229
230        # 3. replace parameter name with "1" and try to evaluate the expression
231        try:
232            expression_to_evaluate = constraint_text.replace(param_str, "1.0")
233            eval(expression_to_evaluate)
234        except Exception:
235            # Too many cases to cover individually, just a blanket
236            # Exception should be sufficient
237            # Note that in current numpy things like sqrt(-1) don't
238            # raise but just return warnings
239            return False
240
241        return True
242
243    def constraint(self):
244        """
245        Return the generated constraint
246        """
247        param = self.cbParam1.currentText()
248        value = self.cbParam2.currentText()
249        func = self.txtConstraint.text()
250        value_ex = self.txtName2.text() + "." + self.cbParam2.currentText()
251        model1 = self.txtName1.text()
252        operator = self.cbOperator.currentText()
253
254        con = Constraint(self,
255                         param=param,
256                         value=value,
257                         func=func,
258                         value_ex=value_ex,
259                         operator=operator)
260
261        return (model1, con)
262
263    def onApply(self):
264        """
265        Respond to Add constraint action.
266        Send a signal that the constraint is ready to be applied
267        """
268        cons_tuple = self.constraint()
269        self.constraintReadySignal.emit(cons_tuple)
270        # reload the comboboxes
271        self.setupParamWidgets()
272
273    def onSetAll(self):
274        """
275        Set constraints on all identically named parameters between two fitpages
276        """
277        # loop over parameters in constrained model
278        items1 = [param for param in self.params[0] if not self.tabs[0].paramHasConstraint(param)]
279        #items2 = [param for param in self.params[1] if not self.tabs[1].paramHasConstraint(i)]
280        items2 = self.params[1]
281        for item in items1:
282            if item not in items2: continue
283            param = item
284            value = item
285            func = self.txtName2.text() + "." + param
286            value_ex = self.txtName1.text() + "." + param
287            model1 = self.txtName1.text()
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()
301
302    def onHelp(self):
303        """
304        Display related help section
305        """
306        try:
307            help_location = GuiUtils.HELP_DIRECTORY_LOCATION + \
308            "/user/qtgui/Perspectives/Fitting/fitting_help.html#simultaneous-fits-with-constraints"
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.