diff options
Diffstat (limited to 'src/silx/gui/fit/Parameters.py')
-rw-r--r-- | src/silx/gui/fit/Parameters.py | 946 |
1 files changed, 946 insertions, 0 deletions
diff --git a/src/silx/gui/fit/Parameters.py b/src/silx/gui/fit/Parameters.py new file mode 100644 index 0000000..bd2605e --- /dev/null +++ b/src/silx/gui/fit/Parameters.py @@ -0,0 +1,946 @@ +# /*########################################################################## +# Copyright (C) 2004-2023 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. +# +# ######################################################################### */ +"""This module defines a table widget that is specialized in displaying fit +parameter results and associated constraints.""" +__authors__ = ["V.A. Sole", "P. Knobel"] +__license__ = "MIT" +__date__ = "25/11/2016" + +import sys + +from silx.gui import qt +from silx.gui.widgets.TableWidget import TableWidget + + +def float_else_zero(sstring): + """Return converted string to float. If conversion fail, return zero. + + :param sstring: String to be converted + :return: ``float(sstrinq)`` if ``sstring`` can be converted to float + (e.g. ``"3.14"``), else ``0`` + """ + try: + return float(sstring) + except ValueError: + return 0 + + +class QComboTableItem(qt.QComboBox): + """:class:`qt.QComboBox` augmented with a ``sigCellChanged`` signal + to emit a tuple of ``(row, column)`` coordinates when the value is + changed. + + This signal can be used to locate the modified combo box in a table. + + :param row: Row number of the table cell containing this widget + :param col: Column number of the table cell containing this widget""" + + sigCellChanged = qt.Signal(int, int) + """Signal emitted when this ``QComboBox`` is activated. + A ``(row, column)`` tuple is passed.""" + + def __init__(self, parent=None, row=None, col=None): + self._row = row + self._col = col + qt.QComboBox.__init__(self, parent) + self.activated[int].connect(self._cellChanged) + + def _cellChanged(self, idx): # noqa + self.sigCellChanged.emit(self._row, self._col) + + +class QCheckBoxItem(qt.QCheckBox): + """:class:`qt.QCheckBox` augmented with a ``sigCellChanged`` signal + to emit a tuple of ``(row, column)`` coordinates when the check box has + been clicked on. + + This signal can be used to locate the modified check box in a table. + + :param row: Row number of the table cell containing this widget + :param col: Column number of the table cell containing this widget""" + + sigCellChanged = qt.Signal(int, int) + """Signal emitted when this ``QCheckBox`` is clicked. + A ``(row, column)`` tuple is passed.""" + + def __init__(self, parent=None, row=None, col=None): + self._row = row + self._col = col + qt.QCheckBox.__init__(self, parent) + self.clicked.connect(self._cellChanged) + + def _cellChanged(self): + self.sigCellChanged.emit(self._row, self._col) + + +class Parameters(TableWidget): + """:class:`TableWidget` customized to display fit results + and to interact with :class:`FitManager` objects. + + Data and references to cell widgets are kept in a dictionary + attribute :attr:`parameters`. + + :param parent: Parent widget + :param labels: Column headers. If ``None``, default headers will be used. + :type labels: List of strings or None + :param paramlist: List of fit parameters to be displayed for each fitted + peak. + :type paramlist: list[str] or None + """ + + def __init__(self, parent=None, paramlist=None): + TableWidget.__init__(self, parent) + self.setContentsMargins(0, 0, 0, 0) + + labels = [ + "Parameter", + "Estimation", + "Fit Value", + "Sigma", + "Constraints", + "Min/Parame", + "Max/Factor/Delta", + ] + tooltips = [ + "Fit parameter name", + "Estimated value for fit parameter. You can edit this column.", + "Actual value for parameter, after fit", + "Uncertainty (same unit as the parameter)", + "Constraint to be applied to the parameter for fit", + "First parameter for constraint (name of another param or min value)", + "Second parameter for constraint (max value, or factor/delta)", + ] + + self.columnKeys = [ + "name", + "estimation", + "fitresult", + "sigma", + "code", + "val1", + "val2", + ] + """This list assigns shorter keys to refer to columns than the + displayed labels.""" + + self.__configuring = False + + # column headers and associated tooltips + self.setColumnCount(len(labels)) + + for i, label in enumerate(labels): + item = self.horizontalHeaderItem(i) + if item is None: + item = qt.QTableWidgetItem(label, qt.QTableWidgetItem.Type) + self.setHorizontalHeaderItem(i, item) + + item.setText(label) + if tooltips is not None: + item.setToolTip(tooltips[i]) + + # resize columns + for col_key in ["name", "estimation", "sigma", "val1", "val2"]: + col_idx = self.columnIndexByField(col_key) + self.resizeColumnToContents(col_idx) + + # Initialize the table with one line per supplied parameter + paramlist = paramlist if paramlist is not None else [] + self.parameters = {} + """This attribute stores all the data in an ordered dictionary. + New data can be added using :meth:`newParameterLine`. + Existing data can be modified using :meth:`configureLine` + + Keys of the dictionary are: + + - 'name': parameter name + - 'line': line index for the parameter in the table + - 'estimation' + - 'fitresult' + - 'sigma' + - 'code': constraint code (one of the elements of + :attr:`code_options`) + - 'val1': first parameter related to constraint, formatted + as a string, as typed in the table + - 'val2': second parameter related to constraint, formatted + as a string, as typed in the table + - 'cons1': scalar representation of 'val1' + (e.g. when val1 is the name of a fit parameter, cons1 + will be the line index of this parameter) + - 'cons2': scalar representation of 'val2' + - 'vmin': equal to 'val1' when 'code' is "QUOTED" + - 'vmax': equal to 'val2' when 'code' is "QUOTED" + - 'relatedto': name of related parameter when this parameter + is constrained to another parameter (same as 'val1') + - 'factor': same as 'val2' when 'code' is 'FACTOR' + - 'delta': same as 'val2' when 'code' is 'DELTA' + - 'sum': same as 'val2' when 'code' is 'SUM' + - 'group': group index for the parameter + - 'xmin': data range minimum + - 'xmax': data range maximum + """ + for line, param in enumerate(paramlist): + self.newParameterLine(param, line) + + self.code_options = [ + "FREE", + "POSITIVE", + "QUOTED", + "FIXED", + "FACTOR", + "DELTA", + "SUM", + "IGNORE", + "ADD", + ] + """Possible values in the combo boxes in the 'Constraints' column. + """ + + # connect signal + self.cellChanged[int, int].connect(self.onCellChanged) + + def newParameterLine(self, param, line): + """Add a line to the :class:`QTableWidget`. + + Each line represents one of the fit parameters for one of + the fitted peaks. + + :param param: Name of the fit parameter + :type param: str + :param line: 0-based line index + :type line: int + """ + # get current number of lines + nlines = self.rowCount() + self.__configuring = True + if line >= nlines: + self.setRowCount(line + 1) + + # default configuration for fit parameters + self.parameters[param] = dict( + ( + ("line", line), + ("estimation", "0"), + ("fitresult", ""), + ("sigma", ""), + ("code", "FREE"), + ("val1", ""), + ("val2", ""), + ("cons1", 0), + ("cons2", 0), + ("vmin", "0"), + ("vmax", "1"), + ("relatedto", ""), + ("factor", "1.0"), + ("delta", "0.0"), + ("sum", "0.0"), + ("group", ""), + ("name", param), + ("xmin", None), + ("xmax", None), + ) + ) + self.setReadWrite(param, "estimation") + self.setReadOnly(param, ["name", "fitresult", "sigma", "val1", "val2"]) + + # Constraint codes + a = [] + for option in self.code_options: + a.append(option) + + code_column_index = self.columnIndexByField("code") + cellWidget = self.cellWidget(line, code_column_index) + if cellWidget is None: + cellWidget = QComboTableItem(self, row=line, col=code_column_index) + cellWidget.addItems(a) + self.setCellWidget(line, code_column_index, cellWidget) + cellWidget.sigCellChanged[int, int].connect(self.onCellChanged) + self.parameters[param]["code_item"] = cellWidget + self.parameters[param]["relatedto_item"] = None + self.__configuring = False + + def columnIndexByField(self, field): + """ + + :param field: Field name (column key) + :return: Index of the column with this field name + """ + return self.columnKeys.index(field) + + def fillFromFit(self, fitresults): + """Fill table with values from a list of dictionaries + (see :attr:`silx.math.fit.fitmanager.FitManager.fit_results`) + + :param fitresults: List of parameters as recorded + in the ``paramlist`` attribute of a :class:`FitManager` object + :type fitresults: list[dict] + """ + self.setRowCount(len(fitresults)) + + # Reinitialize and fill self.parameters + self.parameters = {} + for line, param in enumerate(fitresults): + self.newParameterLine(param["name"], line) + + for param in fitresults: + name = param["name"] + code = str(param["code"]) + if code not in self.code_options: + # convert code from int to descriptive string + code = self.code_options[int(code)] + val1 = param["cons1"] + val2 = param["cons2"] + estimation = param["estimation"] + group = param["group"] + sigma = param["sigma"] + fitresult = param["fitresult"] + + xmin = param.get("xmin") + xmax = param.get("xmax") + + self.configureLine( + name=name, + code=code, + val1=val1, + val2=val2, + estimation=estimation, + fitresult=fitresult, + sigma=sigma, + group=group, + xmin=xmin, + xmax=xmax, + ) + + def getConfiguration(self): + """Return ``FitManager.paramlist`` dictionary + encapsulated in another dictionary""" + return {"parameters": self.getFitResults()} + + def setConfiguration(self, ddict): + """Fill table with values from a ``FitManager.paramlist`` dictionary + encapsulated in another dictionary""" + self.fillFromFit(ddict["parameters"]) + + def getFitResults(self): + """Return fit parameters as a list of dictionaries in the format used + by :class:`FitManager` (attribute ``paramlist``). + """ + fitparameterslist = [] + for param in self.parameters: + fitparam = {} + name = param + estimation, [code, cons1, cons2] = self.getEstimationConstraints(name) + buf = str(self.parameters[param]["fitresult"]) + xmin = self.parameters[param]["xmin"] + xmax = self.parameters[param]["xmax"] + if len(buf): + fitresult = float(buf) + else: + fitresult = 0.0 + buf = str(self.parameters[param]["sigma"]) + if len(buf): + sigma = float(buf) + else: + sigma = 0.0 + buf = str(self.parameters[param]["group"]) + if len(buf): + group = float(buf) + else: + group = 0 + fitparam["name"] = name + fitparam["estimation"] = estimation + fitparam["fitresult"] = fitresult + fitparam["sigma"] = sigma + fitparam["group"] = group + fitparam["code"] = code + fitparam["cons1"] = cons1 + fitparam["cons2"] = cons2 + fitparam["xmin"] = xmin + fitparam["xmax"] = xmax + fitparameterslist.append(fitparam) + return fitparameterslist + + def onCellChanged(self, row, col): + """Slot called when ``cellChanged`` signal is emitted. + Checks the validity of the new text in the cell, then calls + :meth:`configureLine` to update the internal ``self.parameters`` + dictionary. + + :param row: Row number of the changed cell (0-based index) + :param col: Column number of the changed cell (0-based index) + """ + if (col != self.columnIndexByField("code")) and (col != -1): + if row != self.currentRow(): + return + if col != self.currentColumn(): + return + if self.__configuring: + return + param = list(self.parameters)[row] + field = self.columnKeys[col] + oldvalue = self.parameters[param][field] + if col != 4: + item = self.item(row, col) + if item is not None: + newvalue = item.text() + else: + newvalue = "" + else: + # this is the combobox + widget = self.cellWidget(row, col) + newvalue = widget.currentText() + if self.validate(param, field, oldvalue, newvalue): + paramdict = {"name": param, field: newvalue} + self.configureLine(**paramdict) + else: + if field == "code": + # New code not valid, try restoring the old one + index = self.code_options.index(oldvalue) + self.__configuring = True + try: + self.parameters[param]["code_item"].setCurrentIndex(index) + finally: + self.__configuring = False + else: + paramdict = {"name": param, field: oldvalue} + self.configureLine(**paramdict) + + def validate(self, param, field, oldvalue, newvalue): + """Check validity of ``newvalue`` when a cell's value is modified. + + :param param: Fit parameter name + :param field: Column name + :param oldvalue: Cell value before change attempt + :param newvalue: New value to be validated + :return: True if new cell value is valid, else False + """ + if field == "code": + return self.setCodeValue(param, oldvalue, newvalue) + # FIXME: validate() shouldn't have side effects. Move this bit to configureLine()? + if field == "val1" and str(self.parameters[param]["code"]) in [ + "DELTA", + "FACTOR", + "SUM", + ]: + _, candidates = self.getRelatedCandidates(param) + # We expect val1 to be a fit parameter name + if str(newvalue) in candidates: + return True + else: + return False + # except for code, val1 and name (which is read-only and does not need + # validation), all fields must always be convertible to float + else: + try: + float(str(newvalue)) + except ValueError: + return False + return True + + def setCodeValue(self, param, oldvalue, newvalue): + """Update 'code' and 'relatedto' fields when code cell is + changed. + + :param param: Fit parameter name + :param oldvalue: Cell value before change attempt + :param newvalue: New value to be validated + :return: ``True`` if code was successfully updated + """ + + if str(newvalue) in ["FREE", "POSITIVE", "QUOTED", "FIXED"]: + self.configureLine(name=param, code=newvalue) + if str(oldvalue) == "IGNORE": + self.freeRestOfGroup(param) + return True + elif str(newvalue) in ["FACTOR", "DELTA", "SUM"]: + # I should check here that some parameter is set + best, candidates = self.getRelatedCandidates(param) + if len(candidates) == 0: + return False + self.configureLine(name=param, code=newvalue, relatedto=best) + if str(oldvalue) == "IGNORE": + self.freeRestOfGroup(param) + return True + + elif str(newvalue) == "IGNORE": + # I should check if the group can be ignored + # for the time being I just fix all of them to ignore + group = int(float(str(self.parameters[param]["group"]))) + candidates = [] + for param in self.parameters.keys(): + if group == int(float(str(self.parameters[param]["group"]))): + candidates.append(param) + # print candidates + # I should check here if there is any relation to them + for param in candidates: + self.configureLine(name=param, code=newvalue) + return True + elif str(newvalue) == "ADD": + group = int(float(str(self.parameters[param]["group"]))) + if group == 0: + # One cannot add a background group + return False + i = 0 + for param in self.parameters: + if i <= int(float(str(self.parameters[param]["group"]))): + i += 1 + if (group == 0) and (i == 1): # FIXME: why +1? + i += 1 + self.addGroup(i, group) + return False + elif str(newvalue) == "SHOW": + print(self.getEstimationConstraints(param)) + return False + + def addGroup(self, newg, gtype): + """Add a fit parameter group with the same fit parameters as an + existing group. + + This function is called when the user selects "ADD" in the + "constraints" combobox. + + :param int newg: New group number + :param int gtype: Group number whose parameters we want to copy + + """ + newparam = [] + # loop through parameters until we encounter group number `gtype` + for param in list(self.parameters): + paramgroup = int(float(str(self.parameters[param]["group"]))) + # copy parameter names in group number `gtype` + if paramgroup == gtype: + # but replace `gtype` with `newg` + newparam.append(param.rstrip("0123456789") + "%d" % newg) + + xmin = self.parameters[param]["xmin"] + xmax = self.parameters[param]["xmax"] + + # Add new parameters (one table line per parameter) and configureLine each + # one by updating xmin and xmax to the same values as group `gtype` + line = len(list(self.parameters)) + for param in newparam: + self.newParameterLine(param, line) + line += 1 + for param in newparam: + self.configureLine(name=param, group=newg, xmin=xmin, xmax=xmax) + + def freeRestOfGroup(self, workparam): + """Set ``code`` to ``"FREE"`` for all fit parameters belonging to + the same group as ``workparam``. This is done when the entire group + of parameters was previously ignored and one of them has his code + set to something different than ``"IGNORE"``. + + :param workparam: Fit parameter name + """ + if workparam in self.parameters.keys(): + group = int(float(str(self.parameters[workparam]["group"]))) + for param in self.parameters: + if param != workparam and group == int( + float(str(self.parameters[param]["group"])) + ): + self.configureLine( + name=param, code="FREE", cons1=0, cons2=0, val1="", val2="" + ) + + def getRelatedCandidates(self, workparam): + """If fit parameter ``workparam`` has a constraint that involves other + fit parameters, find possible candidates and try to guess which one + is the most likely. + + :param workparam: Fit parameter name + :return: (best_candidate, possible_candidates) tuple + :rtype: (str, list[str]) + """ + candidates = [] + for param_name in self.parameters: + if param_name != workparam: + # ignore parameters that are fixed by a constraint + if str(self.parameters[param_name]["code"]) not in [ + "IGNORE", + "FACTOR", + "DELTA", + "SUM", + ]: + candidates.append(param_name) + # take the previous one (before code cell changed) if possible + if str(self.parameters[workparam]["relatedto"]) in candidates: + best = str(self.parameters[workparam]["relatedto"]) + return best, candidates + # take the first with same base name (after removing numbers) + for param_name in candidates: + basename = param_name.rstrip("0123456789") + try: + pos = workparam.index(basename) + if pos == 0: + best = param_name + return best, candidates + except ValueError: + pass + # take the first + return candidates[0], candidates + + def setReadOnly(self, parameter, fields): + """Make table cells read-only by setting it's flags and omitting + flag ``qt.Qt.ItemIsEditable`` + + :param parameter: Fit parameter names identifying the rows + :type parameter: str or list[str] + :param fields: Field names identifying the columns + :type fields: str or list[str] + """ + editflags = qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled + self.setField(parameter, fields, editflags) + + def setReadWrite(self, parameter, fields): + """Make table cells read-write by setting it's flags including + flag ``qt.Qt.ItemIsEditable`` + + :param parameter: Fit parameter names identifying the rows + :type parameter: str or list[str] + :param fields: Field names identifying the columns + :type fields: str or list[str] + """ + editflags = qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled | qt.Qt.ItemIsEditable + self.setField(parameter, fields, editflags) + + def setField(self, parameter, fields, edit_flags): + """Set text and flags in a table cell. + + :param parameter: Fit parameter names identifying the rows + :type parameter: str or list[str] + :param fields: Field names identifying the columns + :type fields: str or list[str] + :param edit_flags: Flag combination, e.g:: + + qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled | + qt.Qt.ItemIsEditable + """ + if isinstance(parameter, list) or isinstance(parameter, tuple): + paramlist = parameter + else: + paramlist = [parameter] + if isinstance(fields, list) or isinstance(fields, tuple): + fieldlist = fields + else: + fieldlist = [fields] + + # Set _configuring flag to ignore cellChanged signals in + # self.onCellChanged + _oldvalue = self.__configuring + self.__configuring = True + + # 2D loop through parameter list and field list + # to update their cells + for param in paramlist: + row = list(self.parameters.keys()).index(param) + for field in fieldlist: + col = self.columnIndexByField(field) + if field != "code": + key = field + "_item" + item = self.item(row, col) + if item is None: + item = qt.QTableWidgetItem() + item.setText(self.parameters[param][field]) + self.setItem(row, col, item) + else: + item.setText(self.parameters[param][field]) + self.parameters[param][key] = item + item.setFlags(edit_flags) + + # Restore previous _configuring flag + self.__configuring = _oldvalue + + def configureLine( + self, + name, + code=None, + val1=None, + val2=None, + sigma=None, + estimation=None, + fitresult=None, + group=None, + xmin=None, + xmax=None, + relatedto=None, + cons1=None, + cons2=None, + ): + """This function updates values in a line of the table + + :param name: Name of the parameter (serves as unique identifier for + a line). + :param code: Constraint code *FREE, FIXED, POSITIVE, DELTA, FACTOR, + SUM, QUOTED, IGNORE* + :param val1: Constraint 1 (can be the index or name of another + parameter for code *DELTA, FACTOR, SUM*, or a min value + for code *QUOTED*) + :param val2: Constraint 2 + :param sigma: Standard deviation for a fit parameter + :param estimation: Estimated initial value for a fit parameter (used + as input to iterative fit) + :param fitresult: Final result of fit + :param group: Group number of a fit parameter (peak number when doing + multi-peak fitting, as each peak corresponds to a group + of several consecutive parameters) + :param xmin: + :param xmax: + :param relatedto: Index or name of another fit parameter + to which this parameter is related to (constraints) + :param cons1: similar meaning to ``val1``, but is always a number + :param cons2: similar meaning to ``val2``, but is always a number + :return: + """ + paramlist = list(self.parameters.keys()) + + if name not in self.parameters: + raise KeyError("'%s' is not in the parameter list" % name) + + # update code first, if specified + if code is not None: + code = str(code) + self.parameters[name]["code"] = code + # update combobox + index = self.parameters[name]["code_item"].findText(code) + self.parameters[name]["code_item"].setCurrentIndex(index) + else: + # set code to previous value, used later for setting val1 val2 + code = self.parameters[name]["code"] + + # val1 and sigma have special formats + if val1 is not None: + fmt = ( + None + if self.parameters[name]["code"] in ["DELTA", "FACTOR", "SUM"] + else "%8g" + ) + self._updateField(name, "val1", val1, fmat=fmt) + + if sigma is not None: + self._updateField(name, "sigma", sigma, fmat="%6.3g") + + # other fields are formatted as "%8g" + keys_params = ( + ("val2", val2), + ("estimation", estimation), + ("fitresult", fitresult), + ) + for key, value in keys_params: + if value is not None: + self._updateField(name, key, value, fmat="%8g") + + # the rest of the parameters are treated as strings and don't need + # validation + keys_params = ( + ("group", group), + ("xmin", xmin), + ("xmax", xmax), + ("relatedto", relatedto), + ("cons1", cons1), + ("cons2", cons2), + ) + for key, value in keys_params: + if value is not None: + self.parameters[name][key] = str(value) + + # val1 and val2 have different meanings depending on the code + if code == "QUOTED": + if val1 is not None: + self.parameters[name]["vmin"] = self.parameters[name]["val1"] + else: + self.parameters[name]["val1"] = self.parameters[name]["vmin"] + if val2 is not None: + self.parameters[name]["vmax"] = self.parameters[name]["val2"] + else: + self.parameters[name]["val2"] = self.parameters[name]["vmax"] + + # cons1 and cons2 are scalar representations of val1 and val2 + self.parameters[name]["cons1"] = float_else_zero( + self.parameters[name]["val1"] + ) + self.parameters[name]["cons2"] = float_else_zero( + self.parameters[name]["val2"] + ) + + # cons1, cons2 = min(val1, val2), max(val1, val2) + if self.parameters[name]["cons1"] > self.parameters[name]["cons2"]: + self.parameters[name]["cons1"], self.parameters[name]["cons2"] = ( + self.parameters[name]["cons2"], + self.parameters[name]["cons1"], + ) + + elif code in ["DELTA", "SUM", "FACTOR"]: + # For these codes, val1 is the fit parameter name on which the + # constraint depends + if val1 is not None and val1 in paramlist: + self.parameters[name]["relatedto"] = self.parameters[name]["val1"] + + elif val1 is not None: + # val1 could be the index of the fit parameter + try: + self.parameters[name]["relatedto"] = paramlist[int(val1)] + except ValueError: + self.parameters[name]["relatedto"] = self.parameters[name]["val1"] + + elif relatedto is not None: + # code changed, val1 not specified but relatedto specified: + # set val1 to relatedto (pre-fill best guess) + self.parameters[name]["val1"] = relatedto + + # update fields "delta", "sum" or "factor" + key = code.lower() + self.parameters[name][key] = self.parameters[name]["val2"] + + # FIXME: val1 is sometimes specified as an index rather than a param name + self.parameters[name]["val1"] = self.parameters[name]["relatedto"] + + # cons1 is the index of the fit parameter in the ordered dictionary + if self.parameters[name]["val1"] in paramlist: + self.parameters[name]["cons1"] = paramlist.index( + self.parameters[name]["val1"] + ) + + # cons2 is the constraint value (factor, delta or sum) + try: + self.parameters[name]["cons2"] = float( + str(self.parameters[name]["val2"]) + ) + except ValueError: + self.parameters[name]["cons2"] = 1.0 if code == "FACTOR" else 0.0 + + elif code in ["FREE", "POSITIVE", "IGNORE", "FIXED"]: + self.parameters[name]["val1"] = "" + self.parameters[name]["val2"] = "" + self.parameters[name]["cons1"] = 0 + self.parameters[name]["cons2"] = 0 + + self._updateCellRWFlags(name, code) + + def _updateField(self, name, field, value, fmat=None): + """Update field in ``self.parameters`` dictionary, if the new value + is valid. + + :param name: Fit parameter name + :param field: Field name + :param value: New value to assign + :type value: String + :param fmat: Format string (e.g. "%8g") to be applied if value represents + a scalar. If ``None``, format is not modified. If ``value`` is an + empty string, ``fmat`` is ignored. + """ + if value is not None: + oldvalue = self.parameters[name][field] + if fmat is not None: + newvalue = fmat % float(value) if value != "" else "" + else: + newvalue = value + self.parameters[name][field] = ( + newvalue if self.validate(name, field, oldvalue, newvalue) else oldvalue + ) + + def _updateCellRWFlags(self, name, code=None): + """Set read-only or read-write flags in a row, + depending on the constraint code + + :param name: Fit parameter name identifying the row + :param code: Constraint code, in `'FREE', 'POSITIVE', 'IGNORE',` + `'FIXED', 'FACTOR', 'DELTA', 'SUM', 'ADD'` + :return: + """ + if code in ["FREE", "POSITIVE", "IGNORE", "FIXED"]: + self.setReadWrite(name, "estimation") + self.setReadOnly(name, ["fitresult", "sigma", "val1", "val2"]) + else: + self.setReadWrite(name, ["estimation", "val1", "val2"]) + self.setReadOnly(name, ["fitresult", "sigma"]) + + def getEstimationConstraints(self, param): + """ + Return tuple ``(estimation, constraints)`` where ``estimation`` is the + value in the ``estimate`` field and ``constraints`` are the relevant + constraints according to the active code + """ + estimation = None + constraints = None + if param in self.parameters.keys(): + buf = str(self.parameters[param]["estimation"]) + if len(buf): + estimation = float(buf) + else: + estimation = 0 + if str(self.parameters[param]["code"]) in self.code_options: + code = self.code_options.index(str(self.parameters[param]["code"])) + else: + code = str(self.parameters[param]["code"]) + cons1 = self.parameters[param]["cons1"] + cons2 = self.parameters[param]["cons2"] + constraints = [code, cons1, cons2] + return estimation, constraints + + +def main(args): + from silx.math.fit import fittheories + from silx.math.fit import fitmanager + + try: + from PyMca5 import PyMcaDataDir + except ImportError: + raise ImportError("This demo requires PyMca data. Install PyMca5.") + import numpy + import os + + app = qt.QApplication(args) + tab = Parameters(paramlist=["Height", "Position", "FWHM"]) + tab.showGrid() + tab.configureLine(name="Height", estimation="1234", group=0) + tab.configureLine(name="Position", code="FIXED", group=1) + tab.configureLine(name="FWHM", group=1) + + y = numpy.loadtxt( + os.path.join(PyMcaDataDir.PYMCA_DATA_DIR, "XRFSpectrum.mca") + ) # FIXME + + x = numpy.arange(len(y)) * 0.0502883 - 0.492773 + fit = fitmanager.FitManager() + fit.setdata(x=x, y=y, xmin=20, xmax=150) + + fit.loadtheories(fittheories) + + fit.settheory("ahypermet") + fit.configure( + Yscaling=1.0, + PositiveFwhmFlag=True, + PositiveHeightAreaFlag=True, + FwhmPoints=16, + QuotedPositionFlag=1, + HypermetTails=1, + ) + fit.setbackground("Linear") + fit.estimate() + fit.runfit() + tab.fillFromFit(fit.fit_results) + tab.show() + app.exec() + + +if __name__ == "__main__": + main(sys.argv) |