diff options
author | Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> | 2017-08-18 14:48:52 +0200 |
---|---|---|
committer | Picca Frédéric-Emmanuel <picca@synchrotron-soleil.fr> | 2017-08-18 14:48:52 +0200 |
commit | f7bdc2acff3c13a6d632c28c4569690ab106eed7 (patch) | |
tree | 9d67cdb7152ee4e711379e03fe0546c7c3b97303 /silx/gui/fit/Parameters.py |
Import Upstream version 0.5.0+dfsg
Diffstat (limited to 'silx/gui/fit/Parameters.py')
-rw-r--r-- | silx/gui/fit/Parameters.py | 882 |
1 files changed, 882 insertions, 0 deletions
diff --git a/silx/gui/fit/Parameters.py b/silx/gui/fit/Parameters.py new file mode 100644 index 0000000..62e3278 --- /dev/null +++ b/silx/gui/fit/Parameters.py @@ -0,0 +1,882 @@ +# coding: utf-8 +# /*########################################################################## +# Copyright (C) 2004-2017 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 collections import OrderedDict + +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 = OrderedDict() + """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] = OrderedDict((('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 = OrderedDict() + 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., + 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) |