source: sasview/src/sas/qtgui/Perspectives/Fitting/FittingOptions.py @ 72651df

ESS_GUIESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since 72651df was 8873ab7, checked in by Piotr Rozyczko <piotrrozyczko@…>, 6 years ago

Fixed issue with bumps monitors showing up as algorithm options. SASVIEW-1090.
Also, added check for unsupported algorithms.

  • Property mode set to 100644
File size: 9.9 KB
Line 
1# global
2import sys
3import os
4import types
5import webbrowser
6
7from PyQt5 import QtCore
8from PyQt5 import QtGui
9from PyQt5 import QtWidgets
10
11from sas.qtgui.UI import images_rc
12from sas.qtgui.UI import main_resources_rc
13import sas.qtgui.Utilities.GuiUtils as GuiUtils
14
15from bumps import fitters
16import bumps.options
17
18from sas.qtgui.Perspectives.Fitting.UI.FittingOptionsUI import Ui_FittingOptions
19
20# Set the default optimizer
21fitters.FIT_DEFAULT_ID = 'lm'
22
23
24class FittingOptions(QtWidgets.QDialog, Ui_FittingOptions):
25    """
26    Hard-coded version of the fit options dialog available from BUMPS.
27    This should be make more "dynamic".
28    bumps.options.FIT_FIELDS gives mapping between parameter names, parameter strings and field type
29     (double line edit, integer line edit, combo box etc.), e.g.
30        FIT_FIELDS = dict(
31            samples = ("Samples", parse_int),
32            xtol = ("x tolerance", float))
33
34    bumps.fitters.<algorithm>.settings gives mapping between algorithm, parameter name and default value:
35        e.g.
36        settings = [('steps', 1000), ('starts', 1), ('radius', 0.15), ('xtol', 1e-6), ('ftol', 1e-8)]
37    """
38    fit_option_changed = QtCore.pyqtSignal(str)
39
40    def __init__(self, parent=None, config=None):
41        super(FittingOptions, self).__init__(parent)
42        self.setupUi(self)
43
44        self.config = config
45
46        # no reason to have this widget resizable
47        self.setFixedSize(self.minimumSizeHint())
48
49        self.setWindowTitle("Fit Algorithms")
50
51        # Fill up the algorithm combo, based on what BUMPS says is available
52        self.cbAlgorithm.addItems([n.name for n in fitters.FITTERS if n.id in fitters.FIT_ACTIVE_IDS])
53
54        # Handle the Apply button click
55        self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).clicked.connect(self.onApply)
56        # handle the Help button click
57        self.buttonBox.button(QtWidgets.QDialogButtonBox.Help).clicked.connect(self.onHelp)
58
59        # Handle the combo box changes
60        self.cbAlgorithm.currentIndexChanged.connect(self.onAlgorithmChange)
61
62        # Set the default index
63        default_name = [n.name for n in fitters.FITTERS if n.id == fitters.FIT_DEFAULT_ID][0]
64        default_index = self.cbAlgorithm.findText(default_name)
65        self.cbAlgorithm.setCurrentIndex(default_index)
66        # previous algorithm choice
67        self.previous_index = default_index
68
69        # Assign appropriate validators
70        self.assignValidators()
71
72        # Set defaults
73        self.current_fitter_id = fitters.FIT_DEFAULT_ID
74
75        # OK has to be initialized to True, after initial validator setup
76        self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(True)
77
78    def assignValidators(self):
79        """
80        Use options.FIT_FIELDS to assert which line edit gets what validator
81        """
82        for option in bumps.options.FIT_FIELDS.keys():
83            (f_name, f_type) = bumps.options.FIT_FIELDS[option]
84            validator = None
85            if type(f_type) == types.FunctionType:
86                validator = QtGui.QIntValidator()
87                validator.setBottom(0)
88            elif f_type == float:
89                validator = GuiUtils.DoubleValidator()
90                validator.setBottom(0)
91            else:
92                continue
93            for fitter_id in fitters.FIT_ACTIVE_IDS:
94                line_edit = self.widgetFromOption(str(option), current_fitter=str(fitter_id))
95                if hasattr(line_edit, 'setValidator') and validator is not None:
96                    line_edit.setValidator(validator)
97                    line_edit.textChanged.connect(self.check_state)
98                    line_edit.textChanged.emit(line_edit.text())
99
100    def check_state(self, *args, **kwargs):
101        sender = self.sender()
102        validator = sender.validator()
103        state = validator.validate(sender.text(), 0)[0]
104        if state == QtGui.QValidator.Acceptable:
105            color = '' # default
106            self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(True)
107        else:
108            color = '#fff79a' # yellow
109            self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(False)
110
111        sender.setStyleSheet('QLineEdit { background-color: %s }' % color)
112
113    def onAlgorithmChange(self, index):
114        """
115        Change the page in response to combo box index
116        """
117        # Find the algorithm ID from name
118        self.current_fitter_id = \
119            [n.id for n in fitters.FITTERS if n.name == str(self.cbAlgorithm.currentText())][0]
120
121        # find the right stacked widget
122        widget_name = "self.page_"+str(self.current_fitter_id)
123
124        # Convert the name into widget instance
125        try:
126            widget_to_activate = eval(widget_name)
127        except AttributeError:
128            # We don't yet have this optimizer.
129            # Show message
130            msg = "This algorithm has not yet been implemented in SasView.\n"
131            msg += "Please choose a different algorithm"
132            QtWidgets.QMessageBox.warning(self,
133                                        'Warning',
134                                        msg,
135                                        QtWidgets.QMessageBox.Ok)
136            # Move the index to previous position
137            self.cbAlgorithm.setCurrentIndex(self.previous_index)
138            return
139
140        index_for_this_id = self.stackedWidget.indexOf(widget_to_activate)
141
142        # Select the requested widget
143        self.stackedWidget.setCurrentIndex(index_for_this_id)
144
145        self.updateWidgetFromBumps(self.current_fitter_id)
146
147        self.assignValidators()
148
149        # OK has to be reinitialized to True
150        self.buttonBox.button(QtWidgets.QDialogButtonBox.Ok).setEnabled(True)
151
152        # keep reference
153        self.previous_index = index
154
155    def onApply(self):
156        """
157        Update the fitter object
158        """
159        options = self.config.values[self.current_fitter_id]
160        for option in options.keys():
161            # Find the widget name of the option
162            # e.g. 'samples' for 'dream' is 'self.samples_dream'
163            widget_name = 'self.'+option+'_'+self.current_fitter_id
164            try:
165                line_edit = eval(widget_name)
166            except AttributeError:
167                # Skip bumps monitors
168                continue
169            if line_edit is None or not isinstance(line_edit, QtWidgets.QLineEdit):
170                continue
171            color = line_edit.palette().color(QtGui.QPalette.Background).name()
172            if color == '#fff79a':
173                # Show a custom tooltip and return
174                tooltip = "<html><b>Please enter valid values in all fields.</html>"
175                QtWidgets.QToolTip.showText(line_edit.mapToGlobal(
176                    QtCore.QPoint(line_edit.rect().right(), line_edit.rect().bottom() + 2)), tooltip)
177                return
178
179        # Notify the perspective, so the window title is updated
180        self.fit_option_changed.emit(self.cbAlgorithm.currentText())
181
182        def bumpsUpdate(option):
183            """
184            Utility method for bumps state update
185            """
186            widget = self.widgetFromOption(option)
187            if widget is None:
188                return
189            try:
190                if isinstance(widget, QtWidgets.QComboBox):
191                    new_value = widget.currentText()
192                else:
193                    try:
194                        new_value = int(widget.text())
195                    except ValueError:
196                        new_value = float(widget.text())
197                #new_value = widget.currentText() if isinstance(widget, QtWidgets.QComboBox) \
198                #    else float(widget.text())
199                self.config.values[self.current_fitter_id][option] = new_value
200            except ValueError:
201                # Don't update bumps if widget has bad data
202                self.reject
203
204        # Update the BUMPS singleton
205        [bumpsUpdate(o) for o in self.config.values[self.current_fitter_id].keys()]
206        self.close()
207
208    def onHelp(self):
209        """
210        Show the "Fitting options" section of help
211        """
212        tree_location = GuiUtils.HELP_DIRECTORY_LOCATION
213        tree_location += "/user/qtgui/Perspectives/Fitting/"
214
215        # Actual file anchor will depend on the combo box index
216        # Note that we can be clusmy here, since bad current_fitter_id
217        # will just make the page displayed from the top
218        helpfile = "optimizer.html#fit-" + self.current_fitter_id
219        help_location = tree_location + helpfile
220        webbrowser.open('file://' + os.path.realpath(help_location))
221
222    def widgetFromOption(self, option_id, current_fitter=None):
223        """
224        returns widget's element linked to the given option_id
225        """
226        if current_fitter is None:
227            current_fitter = self.current_fitter_id
228        if option_id not in list(bumps.options.FIT_FIELDS.keys()): return None
229        option = option_id + '_' + current_fitter
230        if not hasattr(self, option): return None
231        return eval('self.' + option)
232
233    def getResults(self):
234        """
235        Sends back the current choice of parameters
236        """
237        algorithm = self.cbAlgorithm.currentText()
238        return algorithm
239
240    def updateWidgetFromBumps(self, fitter_id):
241        """
242        Given the ID of the current optimizer, fetch the values
243        and update the widget
244        """
245        options = self.config.values[fitter_id]
246        for option in options.keys():
247            # Find the widget name of the option
248            # e.g. 'samples' for 'dream' is 'self.samples_dream'
249            widget_name = 'self.'+option+'_'+fitter_id
250            if option not in bumps.options.FIT_FIELDS:
251                return
252            if isinstance(bumps.options.FIT_FIELDS[option][1], bumps.options.ChoiceList):
253                control = eval(widget_name)
254                control.setCurrentIndex(control.findText(str(options[option])))
255            else:
256                eval(widget_name).setText(str(options[option]))
257
258        pass
Note: See TracBrowser for help on using the repository browser.