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

ESS_GUI_bumps_abstraction
Last change on this file since fec5842 was 460bdf4, checked in by Piotr Rozyczko <piotr.rozyczko@…>, 6 years ago

Fixed bug for when subsequent C&S jobs would fail.
Modified constraint display to correspond better to the control state.

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