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

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 305114c was 305114c, checked in by Piotr Rozyczko <rozyczko@…>, 22 months ago

Added a warning label to simple and complex constraint widgets,
shown whenever polydisperse parameters are constrained SASVIEW-1042

  • Property mode set to 100644
File size: 7.8 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
17ALLOWED_OPERATORS = ['=','<','>','>=','<=']
18
19# Local UI
20from sas.qtgui.Perspectives.Fitting.UI.ComplexConstraintUI import Ui_ComplexConstraintUI
21
22class ComplexConstraint(QtWidgets.QDialog, Ui_ComplexConstraintUI):
23    def __init__(self, parent=None, tabs=None):
24        super(ComplexConstraint, self).__init__()
25
26        self.setupUi(self)
27        self.setModal(True)
28
29        # Useful globals
30        self.tabs = tabs
31        self.params = None
32        self.tab_names = None
33        self.operator = '='
34
35        self.warning = self.lblWarning.text()
36        self.setupData()
37        self.setupSignals()
38        self.setupWidgets()
39        self.setupTooltip()
40
41        # Default focus is on OK
42        self.cmdOK.setFocus()
43
44    def setupData(self):
45        """
46        Digs into self.tabs and pulls out relevant info
47        """
48        self.tab_names = [tab.kernel_module.name for tab in self.tabs]
49        self.params = [tab.getParamNames() for tab in self.tabs]
50
51    def setupSignals(self):
52        """
53        Signals from various elements
54        """
55        self.cmdOK.clicked.connect(self.accept)
56        self.cmdHelp.clicked.connect(self.onHelp)
57        self.cmdRevert.clicked.connect(self.onRevert)
58        self.txtConstraint.editingFinished.connect(self.validateFormula)
59
60        self.cbParam1.currentIndexChanged.connect(self.onParamIndexChange)
61        self.cbParam2.currentIndexChanged.connect(self.onParamIndexChange)
62        self.cbOperator.currentIndexChanged.connect(self.onOperatorChange)
63
64    def setupWidgets(self):
65        """
66        Setup widgets based on current parameters
67        """
68        self.txtName1.setText(self.tab_names[0])
69        self.txtName2.setText(self.tab_names[1])
70
71        # Show only parameters not already constrained
72        self.cbParam1.clear()
73        items = [param for i,param in enumerate(self.params[0]) if not self.tabs[0].rowHasConstraint(i)]
74        self.cbParam1.addItems(items)
75        self.cbParam2.clear()
76        items = [param for i,param in enumerate(self.params[1]) if not self.tabs[1].rowHasConstraint(i)]
77        self.cbParam2.addItems(items)
78
79        self.txtParam.setText(self.tab_names[0] + ":" + self.cbParam1.currentText())
80
81        self.cbOperator.clear()
82        self.cbOperator.addItems(ALLOWED_OPERATORS)
83        self.txtOperator.setText(self.cbOperator.currentText())
84
85        self.txtConstraint.setText(self.tab_names[1]+"."+self.cbParam2.currentText())
86
87    def setupTooltip(self):
88        """
89        Tooltip for txtConstraint
90        """
91        p1 = self.tab_names[0] + ":" + self.cbParam1.currentText()
92        p2 = self.tab_names[1]+"."+self.cbParam2.currentText()
93        tooltip = "E.g.\n%s = 2.0 * (%s)\n" %(p1, p2)
94        tooltip += "%s = sqrt(%s) + 5"%(p1, p2)
95        self.txtConstraint.setToolTip(tooltip)
96
97    def onParamIndexChange(self, index):
98        """
99        Respond to parameter combo box changes
100        """
101        # Find out the signal source
102        source = self.sender().objectName()
103        param1 = self.cbParam1.currentText()
104        param2 = self.cbParam2.currentText()
105        if source == "cbParam1":
106            self.txtParam.setText(self.tab_names[0] + ":" + param1)
107        else:
108            self.txtConstraint.setText(self.tab_names[1] + "." + param2)
109        # Check if any of the parameters are polydisperse
110        params_list = [param1, param2]
111        all_pars = [tab.model_parameters for tab in self.tabs]
112        is2Ds = [tab.is2D for tab in self.tabs]
113        txt = ""
114        for pars, is2D in zip(all_pars, is2Ds):
115            if any([FittingUtilities.isParamPolydisperse(p, pars, is2D) for p in params_list]):
116                # no parameters are pd - reset the text to not show the warning
117                txt = self.warning
118        self.lblWarning.setText(txt)
119
120
121    def onOperatorChange(self, index):
122        """
123        Respond to operator combo box changes
124        """
125        self.txtOperator.setText(self.cbOperator.currentText())
126
127    def onRevert(self):
128        """
129        switch M1 <-> M2
130        """
131        # Switch parameters
132        self.params[1], self.params[0] = self.params[0], self.params[1]
133        self.tab_names[1], self.tab_names[0] = self.tab_names[0], self.tab_names[1]
134        self.tabs[1], self.tabs[0] = self.tabs[0], self.tabs[1]
135        # Try to swap parameter names in the line edit
136        current_text = self.txtConstraint.text()
137        new_text = current_text.replace(self.cbParam1.currentText(), self.cbParam2.currentText())
138        self.txtConstraint.setText(new_text)
139        # Update labels and tooltips
140        index1 = self.cbParam1.currentIndex()
141        index2 = self.cbParam2.currentIndex()
142        indexOp = self.cbOperator.currentIndex()
143        self.setupWidgets()
144
145        # Original indices
146        self.cbParam1.setCurrentIndex(index2)
147        self.cbParam2.setCurrentIndex(index1)
148        self.cbOperator.setCurrentIndex(indexOp)
149        self.setupTooltip()
150
151    def validateFormula(self):
152        """
153        Add visual cues when formula is incorrect
154        """
155        formula_is_valid = self.validateConstraint(self.txtConstraint.text())
156        if not formula_is_valid:
157            self.cmdOK.setEnabled(False)
158            self.txtConstraint.setStyleSheet("QLineEdit {background-color: red;}")
159        else:
160            self.cmdOK.setEnabled(True)
161            self.txtConstraint.setStyleSheet("QLineEdit {background-color: white;}")
162
163    def validateConstraint(self, constraint_text):
164        """
165        Ensure the constraint has proper form
166        """
167        # 0. none or empty
168        if not constraint_text or not isinstance(constraint_text, str):
169            return False
170
171        # M1.scale  --> model_str='M1', constraint_text='scale'
172        param_str = self.cbParam2.currentText()
173        constraint_text = constraint_text.strip()
174        model_str = self.txtName2.text()
175
176        # 0. Has to contain the model name
177        if model_str != self.txtName2.text():
178            return False
179
180        # Remove model name from constraint
181        constraint_text = constraint_text.replace(model_str+".",'')
182
183        # 1. just the parameter
184        if param_str == constraint_text:
185            return True
186
187        # 2. ensure the text contains parameter name
188        parameter_string_start = constraint_text.find(param_str)
189        if parameter_string_start < 0:
190            return False
191
192        # 3. replace parameter name with "1" and try to evaluate the expression
193        try:
194            expression_to_evaluate = constraint_text.replace(param_str, "1.0")
195            eval(expression_to_evaluate)
196        except Exception:
197            # Too many cases to cover individually, just a blanket
198            # Exception should be sufficient
199            # Note that in current numpy things like sqrt(-1) don't
200            # raise but just return warnings
201            return False
202
203        return True
204
205    def constraint(self):
206        """
207        Return the generated constraint as tuple (model1, param1, operator, constraint)
208        """
209        return (self.txtName1.text(), self.cbParam1.currentText(), self.cbOperator.currentText(), self.txtConstraint.text())
210
211    def onHelp(self):
212        """
213        Display related help section
214        """
215        try:
216            help_location = GuiUtils.HELP_DIRECTORY_LOCATION + \
217            "/user/qtgui/Perspectives/Fitting/fitting_help.html#simultaneous-fits-with-constraints"
218            webbrowser.open('file://' + os.path.realpath(help_location))
219        except AttributeError:
220            # No manager defined - testing and standalone runs
221            pass
222
223
224
Note: See TracBrowser for help on using the repository browser.