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

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

Fixed onSetAll behaviour

  • Property mode set to 100644
File size: 11.0 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
19#ALLOWED_OPERATORS = ['=','<','>','>=','<=']
20ALLOWED_OPERATORS = ['=']
21
22# Local UI
23from sas.qtgui.Perspectives.Fitting.UI.ComplexConstraintUI import Ui_ComplexConstraintUI
24
25class ComplexConstraint(QtWidgets.QDialog, Ui_ComplexConstraintUI):
26    constraintReadySignal = QtCore.pyqtSignal(tuple)
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 = '='
38        self._constraint = Constraint()
39        self.all_menu   = None
40
41        self.warning = self.lblWarning.text()
42        self.setupData()
43        self.setupSignals()
44        self.setupWidgets()
45        self.setupTooltip()
46
47        # Default focus is on OK
48        self.cmdOK.setFocus()
49
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        """
61        self.cmdOK.clicked.connect(self.onApply)
62        self.cmdHelp.clicked.connect(self.onHelp)
63        self.txtConstraint.editingFinished.connect(self.validateFormula)
64        self.cbModel1.currentIndexChanged.connect(self.onModelIndexChange)
65        self.cbModel2.currentIndexChanged.connect(self.onModelIndexChange)
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        """
75        self.cbModel1.insertItems(0, self.tab_names)
76        self.cbModel2.insertItems(0, self.tab_names)
77
78        self.setupParamWidgets()
79
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()
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)
96        self.all_menu.addAction(self.actionAddAll)
97        # https://bugreports.qt.io/browse/QTBUG-13663
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)
102
103    def setupParamWidgets(self):
104        """
105        Fill out comboboxes and set labels with non-constrained parameters
106        """
107        self.cbParam1.clear()
108        tab_index1 = self.cbModel1.currentIndex()
109        items1 = [param for param in self.params[tab_index1] if not self.tabs[tab_index1].paramHasConstraint(param)]
110        self.cbParam1.addItems(items1)
111
112        # M2 doesn't have to be non-constrained
113        self.cbParam2.clear()
114        tab_index2 = self.cbModel2.currentIndex()
115        items2 = [param for param in self.params[tab_index2]]
116        self.cbParam2.addItems(items2)
117
118        self.txtParam.setText(self.tab_names[tab_index1] + ":" + self.cbParam1.currentText())
119
120        self.cbOperator.clear()
121        self.cbOperator.addItems(ALLOWED_OPERATORS)
122        self.txtOperator.setText(self.cbOperator.currentText())
123
124        self.txtConstraint.setText(self.tab_names[tab_index2]+"."+self.cbParam2.currentText())
125
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
137    def setupTooltip(self):
138        """
139        Tooltip for txtConstraint
140        """
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)
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()
153        param1 = self.cbParam1.currentText()
154        param2 = self.cbParam2.currentText()
155        if source == "cbParam1":
156            self.txtParam.setText(self.tab_names[0] + ":" + param1)
157        else:
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
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()
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        """
189        # temporarily disable validation
190        return
191        #
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
208        # M1.scale --> model_str='M1', constraint_text='scale'
209        param_str = self.cbParam2.currentText()
210        constraint_text = constraint_text.strip()
211        model_str = self.cbModel2.currentText()
212
213        # 0. Has to contain the model name
214        if model_str != model_str:
215            return False
216
217        # Remove model name from constraint
218        constraint_text = constraint_text.replace(model_str+".",'')
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
229        # 3. replace parameter name with "1" and try to evaluate the expression
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
242    def constraint(self):
243        """
244        Return the generated constraint
245        """
246        param = self.cbParam1.currentText()
247        value = self.cbParam2.currentText()
248        func = self.txtConstraint.text()
249        value_ex = self.cbModel2.currentText() + "." + self.cbParam2.currentText()
250        model1 = self.cbModel1.currentText()
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
275        """
276        # loop over parameters in constrained model
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]
281        for item in items1:
282            if item not in items2: continue
283            param = item
284            value = item
285            func = self.cbModel2.currentText() + "." + param
286            value_ex = self.cbModel1.currentText() + "." + param
287            model1 = self.cbModel1.currentText()
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.