diff options
Diffstat (limited to 'silx/gui/fit')
-rw-r--r-- | silx/gui/fit/BackgroundWidget.py | 534 | ||||
-rw-r--r-- | silx/gui/fit/FitConfig.py | 543 | ||||
-rw-r--r-- | silx/gui/fit/FitWidget.py | 739 | ||||
-rw-r--r-- | silx/gui/fit/FitWidgets.py | 559 | ||||
-rw-r--r-- | silx/gui/fit/Parameters.py | 882 | ||||
-rw-r--r-- | silx/gui/fit/__init__.py | 28 | ||||
-rw-r--r-- | silx/gui/fit/setup.py | 43 | ||||
-rw-r--r-- | silx/gui/fit/test/__init__.py | 43 | ||||
-rw-r--r-- | silx/gui/fit/test/testBackgroundWidget.py | 83 | ||||
-rw-r--r-- | silx/gui/fit/test/testFitConfig.py | 95 | ||||
-rw-r--r-- | silx/gui/fit/test/testFitWidget.py | 135 |
11 files changed, 0 insertions, 3684 deletions
diff --git a/silx/gui/fit/BackgroundWidget.py b/silx/gui/fit/BackgroundWidget.py deleted file mode 100644 index 76bc043..0000000 --- a/silx/gui/fit/BackgroundWidget.py +++ /dev/null @@ -1,534 +0,0 @@ -# coding: utf-8 -#/*########################################################################## -# Copyright (C) 2004-2020 V.A. Sole, European Synchrotron Radiation Facility -# -# This file is part of the PyMca X-ray Fluorescence Toolkit developed at -# the ESRF by the Software group. -# -# 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 provides a background configuration widget -:class:`BackgroundWidget` and a corresponding dialog window -:class:`BackgroundDialog`. - -.. image:: img/BackgroundDialog.png - :height: 300px -""" -import sys -import numpy -from silx.gui import qt -from silx.gui.plot import PlotWidget -from silx.math.fit import filters - -__authors__ = ["V.A. Sole", "P. Knobel"] -__license__ = "MIT" -__date__ = "28/06/2017" - - -class HorizontalSpacer(qt.QWidget): - def __init__(self, *args): - qt.QWidget.__init__(self, *args) - self.setSizePolicy(qt.QSizePolicy(qt.QSizePolicy.Expanding, - qt.QSizePolicy.Fixed)) - - -class BackgroundParamWidget(qt.QWidget): - """Background configuration composite widget. - - Strip and snip filters parameters can be adjusted using input widgets. - - Updating the widgets causes :attr:`sigBackgroundParamWidgetSignal` to - be emitted. - """ - sigBackgroundParamWidgetSignal = qt.pyqtSignal(object) - - def __init__(self, parent=None): - qt.QWidget.__init__(self, parent) - - self.mainLayout = qt.QGridLayout(self) - self.mainLayout.setColumnStretch(1, 1) - - # Algorithm choice --------------------------------------------------- - self.algorithmComboLabel = qt.QLabel(self) - self.algorithmComboLabel.setText("Background algorithm") - self.algorithmCombo = qt.QComboBox(self) - self.algorithmCombo.addItem("Strip") - self.algorithmCombo.addItem("Snip") - self.algorithmCombo.activated[int].connect( - self._algorithmComboActivated) - - # Strip parameters --------------------------------------------------- - self.stripWidthLabel = qt.QLabel(self) - self.stripWidthLabel.setText("Strip Width") - - self.stripWidthSpin = qt.QSpinBox(self) - self.stripWidthSpin.setMaximum(100) - self.stripWidthSpin.setMinimum(1) - self.stripWidthSpin.valueChanged[int].connect(self._emitSignal) - - self.stripIterLabel = qt.QLabel(self) - self.stripIterLabel.setText("Strip Iterations") - self.stripIterValue = qt.QLineEdit(self) - validator = qt.QIntValidator(self.stripIterValue) - self.stripIterValue._v = validator - self.stripIterValue.setText("0") - self.stripIterValue.editingFinished[()].connect(self._emitSignal) - self.stripIterValue.setToolTip( - "Number of iterations for strip algorithm.\n" + - "If greater than 999, an 2nd pass of strip filter is " + - "applied to remove artifacts created by first pass.") - - # Snip parameters ---------------------------------------------------- - self.snipWidthLabel = qt.QLabel(self) - self.snipWidthLabel.setText("Snip Width") - - self.snipWidthSpin = qt.QSpinBox(self) - self.snipWidthSpin.setMaximum(300) - self.snipWidthSpin.setMinimum(0) - self.snipWidthSpin.valueChanged[int].connect(self._emitSignal) - - - # Smoothing parameters ----------------------------------------------- - self.smoothingFlagCheck = qt.QCheckBox(self) - self.smoothingFlagCheck.setText("Smoothing Width (Savitsky-Golay)") - self.smoothingFlagCheck.toggled.connect(self._smoothingToggled) - - self.smoothingSpin = qt.QSpinBox(self) - self.smoothingSpin.setMinimum(3) - #self.smoothingSpin.setMaximum(40) - self.smoothingSpin.setSingleStep(2) - self.smoothingSpin.valueChanged[int].connect(self._emitSignal) - - # Anchors ------------------------------------------------------------ - - self.anchorsGroup = qt.QWidget(self) - anchorsLayout = qt.QHBoxLayout(self.anchorsGroup) - anchorsLayout.setSpacing(2) - anchorsLayout.setContentsMargins(0, 0, 0, 0) - - self.anchorsFlagCheck = qt.QCheckBox(self.anchorsGroup) - self.anchorsFlagCheck.setText("Use anchors") - self.anchorsFlagCheck.setToolTip( - "Define X coordinates of points that must remain fixed") - self.anchorsFlagCheck.stateChanged[int].connect( - self._anchorsToggled) - anchorsLayout.addWidget(self.anchorsFlagCheck) - - maxnchannel = 16384 * 4 # Fixme ? - self.anchorsList = [] - num_anchors = 4 - for i in range(num_anchors): - anchorSpin = qt.QSpinBox(self.anchorsGroup) - anchorSpin.setMinimum(0) - anchorSpin.setMaximum(maxnchannel) - anchorSpin.valueChanged[int].connect(self._emitSignal) - anchorsLayout.addWidget(anchorSpin) - self.anchorsList.append(anchorSpin) - - # Layout ------------------------------------------------------------ - self.mainLayout.addWidget(self.algorithmComboLabel, 0, 0) - self.mainLayout.addWidget(self.algorithmCombo, 0, 2) - self.mainLayout.addWidget(self.stripWidthLabel, 1, 0) - self.mainLayout.addWidget(self.stripWidthSpin, 1, 2) - self.mainLayout.addWidget(self.stripIterLabel, 2, 0) - self.mainLayout.addWidget(self.stripIterValue, 2, 2) - self.mainLayout.addWidget(self.snipWidthLabel, 3, 0) - self.mainLayout.addWidget(self.snipWidthSpin, 3, 2) - self.mainLayout.addWidget(self.smoothingFlagCheck, 4, 0) - self.mainLayout.addWidget(self.smoothingSpin, 4, 2) - self.mainLayout.addWidget(self.anchorsGroup, 5, 0, 1, 4) - - # Initialize interface ----------------------------------------------- - self._setAlgorithm("strip") - self.smoothingFlagCheck.setChecked(False) - self._smoothingToggled(is_checked=False) - self.anchorsFlagCheck.setChecked(False) - self._anchorsToggled(is_checked=False) - - def _algorithmComboActivated(self, algorithm_index): - self._setAlgorithm("strip" if algorithm_index == 0 else "snip") - - def _setAlgorithm(self, algorithm): - """Enable/disable snip and snip input widgets, depending on the - chosen algorithm. - :param algorithm: "snip" or "strip" - """ - if algorithm not in ["strip", "snip"]: - raise ValueError( - "Unknown background filter algorithm %s" % algorithm) - - self.algorithm = algorithm - self.stripWidthSpin.setEnabled(algorithm == "strip") - self.stripIterValue.setEnabled(algorithm == "strip") - self.snipWidthSpin.setEnabled(algorithm == "snip") - - def _smoothingToggled(self, is_checked): - """Enable/disable smoothing input widgets, emit dictionary""" - self.smoothingSpin.setEnabled(is_checked) - self._emitSignal() - - def _anchorsToggled(self, is_checked): - """Enable/disable all spin widgets defining anchor X coordinates, - emit signal. - """ - for anchor_spin in self.anchorsList: - anchor_spin.setEnabled(is_checked) - self._emitSignal() - - def setParameters(self, ddict): - """Set values for all input widgets. - - :param dict ddict: Input dictionary, must have the same - keys as the dictionary output by :meth:`getParameters` - """ - if "algorithm" in ddict: - self._setAlgorithm(ddict["algorithm"]) - - if "SnipWidth" in ddict: - self.snipWidthSpin.setValue(int(ddict["SnipWidth"])) - - if "StripWidth" in ddict: - self.stripWidthSpin.setValue(int(ddict["StripWidth"])) - - if "StripIterations" in ddict: - self.stripIterValue.setText("%d" % int(ddict["StripIterations"])) - - if "SmoothingFlag" in ddict: - self.smoothingFlagCheck.setChecked(bool(ddict["SmoothingFlag"])) - - if "SmoothingWidth" in ddict: - self.smoothingSpin.setValue(int(ddict["SmoothingWidth"])) - - if "AnchorsFlag" in ddict: - self.anchorsFlagCheck.setChecked(bool(ddict["AnchorsFlag"])) - - if "AnchorsList" in ddict: - anchorslist = ddict["AnchorsList"] - if anchorslist in [None, 'None']: - anchorslist = [] - for spin in self.anchorsList: - spin.setValue(0) - - i = 0 - for value in anchorslist: - self.anchorsList[i].setValue(int(value)) - i += 1 - - def getParameters(self): - """Return dictionary of parameters defined in the GUI - - The returned dictionary contains following values: - - - *algorithm*: *"strip"* or *"snip"* - - *StripWidth*: width of strip iterator - - *StripIterations*: number of iterations - - *StripThreshold*: curvature parameter (currently fixed to 1.0) - - *SnipWidth*: width of snip algorithm - - *SmoothingFlag*: flag to enable/disable smoothing - - *SmoothingWidth*: width of Savitsky-Golay smoothing filter - - *AnchorsFlag*: flag to enable/disable anchors - - *AnchorsList*: list of anchors (X coordinates of fixed values) - """ - stripitertext = self.stripIterValue.text() - stripiter = int(stripitertext) if len(stripitertext) else 0 - - return {"algorithm": self.algorithm, - "StripThreshold": 1.0, - "SnipWidth": self.snipWidthSpin.value(), - "StripIterations": stripiter, - "StripWidth": self.stripWidthSpin.value(), - "SmoothingFlag": self.smoothingFlagCheck.isChecked(), - "SmoothingWidth": self.smoothingSpin.value(), - "AnchorsFlag": self.anchorsFlagCheck.isChecked(), - "AnchorsList": [spin.value() for spin in self.anchorsList]} - - def _emitSignal(self, dummy=None): - self.sigBackgroundParamWidgetSignal.emit( - {'event': 'ParametersChanged', - 'parameters': self.getParameters()}) - - -class BackgroundWidget(qt.QWidget): - """Background configuration widget, with a plot to preview the results. - - Strip and snip filters parameters can be adjusted using input widgets, - and the computed backgrounds are plotted next to the original data to - show the result.""" - def __init__(self, parent=None): - qt.QWidget.__init__(self, parent) - self.setWindowTitle("Strip and SNIP Configuration Window") - self.mainLayout = qt.QVBoxLayout(self) - self.mainLayout.setContentsMargins(0, 0, 0, 0) - self.mainLayout.setSpacing(2) - self.parametersWidget = BackgroundParamWidget(self) - self.graphWidget = PlotWidget(parent=self) - self.mainLayout.addWidget(self.parametersWidget) - self.mainLayout.addWidget(self.graphWidget) - self._x = None - self._y = None - self.parametersWidget.sigBackgroundParamWidgetSignal.connect(self._slot) - - def getParameters(self): - """Return dictionary of parameters defined in the GUI - - The returned dictionary contains following values: - - - *algorithm*: *"strip"* or *"snip"* - - *StripWidth*: width of strip iterator - - *StripIterations*: number of iterations - - *StripThreshold*: strip curvature (currently fixed to 1.0) - - *SnipWidth*: width of snip algorithm - - *SmoothingFlag*: flag to enable/disable smoothing - - *SmoothingWidth*: width of Savitsky-Golay smoothing filter - - *AnchorsFlag*: flag to enable/disable anchors - - *AnchorsList*: list of anchors (X coordinates of fixed values) - """ - return self.parametersWidget.getParameters() - - def setParameters(self, ddict): - """Set values for all input widgets. - - :param dict ddict: Input dictionary, must have the same - keys as the dictionary output by :meth:`getParameters` - """ - return self.parametersWidget.setParameters(ddict) - - def setData(self, x, y, xmin=None, xmax=None): - """Set data for the original curve, and _update strip and snip - curves accordingly. - - :param x: Array or sequence of curve abscissa values - :param y: Array or sequence of curve ordinate values - :param xmin: Min value to be displayed on the X axis - :param xmax: Max value to be displayed on the X axis - """ - self._x = x - self._y = y - self._xmin = xmin - self._xmax = xmax - self._update(resetzoom=True) - - def _slot(self, ddict): - self._update() - - def _update(self, resetzoom=False): - """Compute strip and snip backgrounds, update the curves - """ - if self._y is None: - return - - pars = self.getParameters() - - # smoothed data - y = numpy.ravel(numpy.array(self._y)).astype(numpy.float64) - if pars["SmoothingFlag"]: - ysmooth = filters.savitsky_golay(y, pars['SmoothingWidth']) - f = [0.25, 0.5, 0.25] - ysmooth[1:-1] = numpy.convolve(ysmooth, f, mode=0) - ysmooth[0] = 0.5 * (ysmooth[0] + ysmooth[1]) - ysmooth[-1] = 0.5 * (ysmooth[-1] + ysmooth[-2]) - else: - ysmooth = y - - - # loop for anchors - x = self._x - niter = pars['StripIterations'] - anchors_indices = [] - if pars['AnchorsFlag'] and pars['AnchorsList'] is not None: - ravelled = x - for channel in pars['AnchorsList']: - if channel <= ravelled[0]: - continue - index = numpy.nonzero(ravelled >= channel)[0] - if len(index): - index = min(index) - if index > 0: - anchors_indices.append(index) - - stripBackground = filters.strip(ysmooth, - w=pars['StripWidth'], - niterations=niter, - factor=pars['StripThreshold'], - anchors=anchors_indices) - - if niter >= 1000: - # final smoothing - stripBackground = filters.strip(stripBackground, - w=1, - niterations=50*pars['StripWidth'], - factor=pars['StripThreshold'], - anchors=anchors_indices) - - if len(anchors_indices) == 0: - anchors_indices = [0, len(ysmooth)-1] - anchors_indices.sort() - snipBackground = 0.0 * ysmooth - lastAnchor = 0 - for anchor in anchors_indices: - if (anchor > lastAnchor) and (anchor < len(ysmooth)): - snipBackground[lastAnchor:anchor] =\ - filters.snip1d(ysmooth[lastAnchor:anchor], - pars['SnipWidth']) - lastAnchor = anchor - if lastAnchor < len(ysmooth): - snipBackground[lastAnchor:] =\ - filters.snip1d(ysmooth[lastAnchor:], - pars['SnipWidth']) - - self.graphWidget.addCurve(x, y, - legend='Input Data', - replace=True, - resetzoom=resetzoom) - self.graphWidget.addCurve(x, stripBackground, - legend='Strip Background', - resetzoom=False) - self.graphWidget.addCurve(x, snipBackground, - legend='SNIP Background', - resetzoom=False) - if self._xmin is not None and self._xmax is not None: - self.graphWidget.getXAxis().setLimits(self._xmin, self._xmax) - - -class BackgroundDialog(qt.QDialog): - """QDialog window featuring a :class:`BackgroundWidget`""" - def __init__(self, parent=None): - qt.QDialog.__init__(self, parent) - self.setWindowTitle("Strip and Snip Configuration Window") - self.mainLayout = qt.QVBoxLayout(self) - self.mainLayout.setContentsMargins(0, 0, 0, 0) - self.mainLayout.setSpacing(2) - self.parametersWidget = BackgroundWidget(self) - self.mainLayout.addWidget(self.parametersWidget) - hbox = qt.QWidget(self) - hboxLayout = qt.QHBoxLayout(hbox) - hboxLayout.setContentsMargins(0, 0, 0, 0) - hboxLayout.setSpacing(2) - self.okButton = qt.QPushButton(hbox) - self.okButton.setText("OK") - self.okButton.setAutoDefault(False) - self.dismissButton = qt.QPushButton(hbox) - self.dismissButton.setText("Cancel") - self.dismissButton.setAutoDefault(False) - hboxLayout.addWidget(HorizontalSpacer(hbox)) - hboxLayout.addWidget(self.okButton) - hboxLayout.addWidget(self.dismissButton) - self.mainLayout.addWidget(hbox) - self.dismissButton.clicked.connect(self.reject) - self.okButton.clicked.connect(self.accept) - - self.output = {} - """Configuration dictionary containing following fields: - - - *SmoothingFlag* - - *SmoothingWidth* - - *StripWidth* - - *StripIterations* - - *StripThreshold* - - *SnipWidth* - - *AnchorsFlag* - - *AnchorsList* - """ - - # self.parametersWidget.parametersWidget.sigBackgroundParamWidgetSignal.connect(self.updateOutput) - - # def updateOutput(self, ddict): - # self.output = ddict - - def accept(self): - """Update :attr:`output`, then call :meth:`QDialog.accept` - """ - self.output = self.getParameters() - super(BackgroundDialog, self).accept() - - def sizeHint(self): - return qt.QSize(int(1.5*qt.QDialog.sizeHint(self).width()), - qt.QDialog.sizeHint(self).height()) - - def setData(self, x, y, xmin=None, xmax=None): - """See :meth:`BackgroundWidget.setData`""" - return self.parametersWidget.setData(x, y, xmin, xmax) - - def getParameters(self): - """See :meth:`BackgroundWidget.getParameters`""" - return self.parametersWidget.getParameters() - - def setParameters(self, ddict): - """See :meth:`BackgroundWidget.setPrintGeometry`""" - return self.parametersWidget.setParameters(ddict) - - def setDefault(self, ddict): - """Alias for :meth:`setPrintGeometry`""" - return self.setParameters(ddict) - - -def getBgDialog(parent=None, default=None, modal=True): - """Instantiate and return a bg configuration dialog, adapted - for configuring standard background theories from - :mod:`silx.math.fit.bgtheories`. - - :return: Instance of :class:`BackgroundDialog` - """ - bgd = BackgroundDialog(parent=parent) - # apply default to newly added pages - bgd.setParameters(default) - - return bgd - - -def main(): - # synthetic data - from silx.math.fit.functions import sum_gauss - - x = numpy.arange(5000) - # (height1, center1, fwhm1, ...) 5 peaks - params1 = (50, 500, 100, - 20, 2000, 200, - 50, 2250, 100, - 40, 3000, 75, - 23, 4000, 150) - y0 = sum_gauss(x, *params1) - - # random values between [-1;1] - noise = 2 * numpy.random.random(5000) - 1 - # make it +- 5% - noise *= 0.05 - - # 2 gaussians with very large fwhm, as background signal - actual_bg = sum_gauss(x, 15, 3500, 3000, 5, 1000, 1500) - - # Add 5% random noise to gaussians and add background - y = y0 + numpy.average(y0) * noise + actual_bg - - # Open widget - a = qt.QApplication(sys.argv) - a.lastWindowClosed.connect(a.quit) - - def mySlot(ddict): - print(ddict) - - w = BackgroundDialog() - w.parametersWidget.parametersWidget.sigBackgroundParamWidgetSignal.connect(mySlot) - w.setData(x, y) - w.exec_() - #a.exec_() - -if __name__ == "__main__": - main() diff --git a/silx/gui/fit/FitConfig.py b/silx/gui/fit/FitConfig.py deleted file mode 100644 index 479e469..0000000 --- a/silx/gui/fit/FitConfig.py +++ /dev/null @@ -1,543 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# Copyright (C) 2004-2018 V.A. Sole, European Synchrotron Radiation Facility -# -# This file is part of the PyMca X-ray Fluorescence Toolkit developed at -# the ESRF by the Software group. -# -# 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 widgets used to build a fit configuration dialog. -The resulting dialog widget outputs a dictionary of configuration parameters. -""" -from silx.gui import qt - -__authors__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "30/11/2016" - - -class TabsDialog(qt.QDialog): - """Dialog widget containing a QTabWidget :attr:`tabWidget` - and a buttons: - - # - buttonHelp - - buttonDefaults - - buttonOk - - buttonCancel - - This dialog defines a __len__ returning the number of tabs, - and an __iter__ method yielding the tab widgets. - """ - def __init__(self, parent=None): - qt.QDialog.__init__(self, parent) - self.tabWidget = qt.QTabWidget(self) - - layout = qt.QVBoxLayout(self) - layout.addWidget(self.tabWidget) - - layout2 = qt.QHBoxLayout(None) - - # self.buttonHelp = qt.QPushButton(self) - # self.buttonHelp.setText("Help") - # layout2.addWidget(self.buttonHelp) - - self.buttonDefault = qt.QPushButton(self) - self.buttonDefault.setText("Undo changes") - layout2.addWidget(self.buttonDefault) - - spacer = qt.QSpacerItem(20, 20, - qt.QSizePolicy.Expanding, - qt.QSizePolicy.Minimum) - layout2.addItem(spacer) - - self.buttonOk = qt.QPushButton(self) - self.buttonOk.setText("OK") - layout2.addWidget(self.buttonOk) - - self.buttonCancel = qt.QPushButton(self) - self.buttonCancel.setText("Cancel") - layout2.addWidget(self.buttonCancel) - - layout.addLayout(layout2) - - self.buttonOk.clicked.connect(self.accept) - self.buttonCancel.clicked.connect(self.reject) - - def __len__(self): - """Return number of tabs""" - return self.tabWidget.count() - - def __iter__(self): - """Return the next tab widget in :attr:`tabWidget` every - time this method is called. - - :return: Tab widget - :rtype: QWidget - """ - for widget_index in range(len(self)): - yield self.tabWidget.widget(widget_index) - - def addTab(self, page, label): - """Add a new tab - - :param page: Content of new page. Must be a widget with - a get() method returning a dictionary. - :param str label: Tab label - """ - self.tabWidget.addTab(page, label) - - def getTabLabels(self): - """ - Return a list of all tab labels in :attr:`tabWidget` - """ - return [self.tabWidget.tabText(i) for i in range(len(self))] - - -class TabsDialogData(TabsDialog): - """This dialog adds a data attribute to :class:`TabsDialog`. - - Data input in widgets, such as text entries or checkboxes, is stored in an - attribute :attr:`output` when the user clicks the OK button. - - A default dictionary can be supplied when this dialog is initialized, to - be used as default data for :attr:`output`. - """ - def __init__(self, parent=None, modal=True, default=None): - """ - - :param parent: Parent :class:`QWidget` - :param modal: If `True`, dialog is modal, meaning this dialog remains - in front of it's parent window and disables it until the user is - done interacting with the dialog - :param default: Default dictionary, used to initialize and reset - :attr:`output`. - """ - TabsDialog.__init__(self, parent) - self.setModal(modal) - self.setWindowTitle("Fit configuration") - - self.output = {} - - self.default = {} if default is None else default - - self.buttonDefault.clicked.connect(self._resetDefault) - # self.keyPressEvent(qt.Qt.Key_Enter). - - def keyPressEvent(self, event): - """Redefining this method to ignore Enter key - (for some reason it activates buttonDefault callback which - resets all widgets) - """ - if event.key() in [qt.Qt.Key_Enter, qt.Qt.Key_Return]: - return - TabsDialog.keyPressEvent(self, event) - - def accept(self): - """When *OK* is clicked, update :attr:`output` with data from - various widgets - """ - self.output.update(self.default) - - # loop over all tab widgets (uses TabsDialog.__iter__) - for tabWidget in self: - self.output.update(tabWidget.get()) - - # avoid pathological None cases - for key in self.output.keys(): - if self.output[key] is None: - if key in self.default: - self.output[key] = self.default[key] - super(TabsDialogData, self).accept() - - def reject(self): - """When the *Cancel* button is clicked, reinitialize :attr:`output` - and quit - """ - self.setDefault() - super(TabsDialogData, self).reject() - - def _resetDefault(self, checked): - self.setDefault() - - def setDefault(self, newdefault=None): - """Reinitialize :attr:`output` with :attr:`default` or with - new dictionary ``newdefault`` if provided. - Call :meth:`setDefault` for each tab widget, if available. - """ - self.output = {} - if newdefault is None: - newdefault = self.default - else: - self.default = newdefault - self.output.update(newdefault) - - for tabWidget in self: - if hasattr(tabWidget, "setDefault"): - tabWidget.setDefault(self.output) - - -class ConstraintsPage(qt.QGroupBox): - """Checkable QGroupBox widget filled with QCheckBox widgets, - to configure the fit estimation for standard fit theories. - """ - def __init__(self, parent=None, title="Set constraints"): - super(ConstraintsPage, self).__init__(parent) - self.setTitle(title) - self.setToolTip("Disable 'Set constraints' to remove all " + - "constraints on all fit parameters") - self.setCheckable(True) - - layout = qt.QVBoxLayout(self) - self.setLayout(layout) - - self.positiveHeightCB = qt.QCheckBox("Force positive height/area", self) - self.positiveHeightCB.setToolTip("Fit must find positive peaks") - layout.addWidget(self.positiveHeightCB) - - self.positionInIntervalCB = qt.QCheckBox("Force position in interval", self) - self.positionInIntervalCB.setToolTip( - "Fit must position peak within X limits") - layout.addWidget(self.positionInIntervalCB) - - self.positiveFwhmCB = qt.QCheckBox("Force positive FWHM", self) - self.positiveFwhmCB.setToolTip("Fit must find a positive FWHM") - layout.addWidget(self.positiveFwhmCB) - - self.sameFwhmCB = qt.QCheckBox("Force same FWHM for all peaks", self) - self.sameFwhmCB.setToolTip("Fit must find same FWHM for all peaks") - layout.addWidget(self.sameFwhmCB) - - self.quotedEtaCB = qt.QCheckBox("Force Eta between 0 and 1", self) - self.quotedEtaCB.setToolTip( - "Fit must find Eta between 0 and 1 for pseudo-Voigt function") - layout.addWidget(self.quotedEtaCB) - - layout.addStretch() - - self.setDefault() - - def setDefault(self, default_dict=None): - """Set default state for all widgets. - - :param default_dict: If a default config dictionary is provided as - a parameter, its values are used as default state.""" - if default_dict is None: - default_dict = {} - # this one uses reverse logic: if checked, NoConstraintsFlag must be False - self.setChecked( - not default_dict.get('NoConstraintsFlag', False)) - self.positiveHeightCB.setChecked( - default_dict.get('PositiveHeightAreaFlag', True)) - self.positionInIntervalCB.setChecked( - default_dict.get('QuotedPositionFlag', False)) - self.positiveFwhmCB.setChecked( - default_dict.get('PositiveFwhmFlag', True)) - self.sameFwhmCB.setChecked( - default_dict.get('SameFwhmFlag', False)) - self.quotedEtaCB.setChecked( - default_dict.get('QuotedEtaFlag', False)) - - def get(self): - """Return a dictionary of constraint flags, to be processed by the - :meth:`configure` method of the selected fit theory.""" - ddict = { - 'NoConstraintsFlag': not self.isChecked(), - 'PositiveHeightAreaFlag': self.positiveHeightCB.isChecked(), - 'QuotedPositionFlag': self.positionInIntervalCB.isChecked(), - 'PositiveFwhmFlag': self.positiveFwhmCB.isChecked(), - 'SameFwhmFlag': self.sameFwhmCB.isChecked(), - 'QuotedEtaFlag': self.quotedEtaCB.isChecked(), - } - return ddict - - -class SearchPage(qt.QWidget): - def __init__(self, parent=None): - super(SearchPage, self).__init__(parent) - layout = qt.QVBoxLayout(self) - - self.manualFwhmGB = qt.QGroupBox("Define FWHM manually", self) - self.manualFwhmGB.setCheckable(True) - self.manualFwhmGB.setToolTip( - "If disabled, the FWHM parameter used for peak search is " + - "estimated based on the highest peak in the data") - layout.addWidget(self.manualFwhmGB) - # ------------ GroupBox fwhm-------------------------- - layout2 = qt.QHBoxLayout(self.manualFwhmGB) - self.manualFwhmGB.setLayout(layout2) - - label = qt.QLabel("Fwhm Points", self.manualFwhmGB) - layout2.addWidget(label) - - self.fwhmPointsSpin = qt.QSpinBox(self.manualFwhmGB) - self.fwhmPointsSpin.setRange(0, 999999) - self.fwhmPointsSpin.setToolTip("Typical peak fwhm (number of data points)") - layout2.addWidget(self.fwhmPointsSpin) - # ---------------------------------------------------- - - self.manualScalingGB = qt.QGroupBox("Define scaling manually", self) - self.manualScalingGB.setCheckable(True) - self.manualScalingGB.setToolTip( - "If disabled, the Y scaling used for peak search is " + - "estimated automatically") - layout.addWidget(self.manualScalingGB) - # ------------ GroupBox scaling----------------------- - layout3 = qt.QHBoxLayout(self.manualScalingGB) - self.manualScalingGB.setLayout(layout3) - - label = qt.QLabel("Y Scaling", self.manualScalingGB) - layout3.addWidget(label) - - self.yScalingEntry = qt.QLineEdit(self.manualScalingGB) - self.yScalingEntry.setToolTip( - "Data values will be multiplied by this value prior to peak" + - " search") - self.yScalingEntry.setValidator(qt.QDoubleValidator(self)) - layout3.addWidget(self.yScalingEntry) - # ---------------------------------------------------- - - # ------------------- grid layout -------------------- - containerWidget = qt.QWidget(self) - layout4 = qt.QHBoxLayout(containerWidget) - containerWidget.setLayout(layout4) - - label = qt.QLabel("Sensitivity", containerWidget) - layout4.addWidget(label) - - self.sensitivityEntry = qt.QLineEdit(containerWidget) - self.sensitivityEntry.setToolTip( - "Peak search sensitivity threshold, expressed as a multiple " + - "of the standard deviation of the noise.\nMinimum value is 1 " + - "(to be detected, peak must be higher than the estimated noise)") - sensivalidator = qt.QDoubleValidator(self) - sensivalidator.setBottom(1.0) - self.sensitivityEntry.setValidator(sensivalidator) - layout4.addWidget(self.sensitivityEntry) - # ---------------------------------------------------- - layout.addWidget(containerWidget) - - self.forcePeakPresenceCB = qt.QCheckBox("Force peak presence", self) - self.forcePeakPresenceCB.setToolTip( - "If peak search algorithm is unsuccessful, place one peak " + - "at the maximum of the curve") - layout.addWidget(self.forcePeakPresenceCB) - - layout.addStretch() - - self.setDefault() - - def setDefault(self, default_dict=None): - """Set default values for all widgets. - - :param default_dict: If a default config dictionary is provided as - a parameter, its values are used as default values.""" - if default_dict is None: - default_dict = {} - self.manualFwhmGB.setChecked( - not default_dict.get('AutoFwhm', True)) - self.fwhmPointsSpin.setValue( - default_dict.get('FwhmPoints', 8)) - self.sensitivityEntry.setText( - str(default_dict.get('Sensitivity', 1.0))) - self.manualScalingGB.setChecked( - not default_dict.get('AutoScaling', False)) - self.yScalingEntry.setText( - str(default_dict.get('Yscaling', 1.0))) - self.forcePeakPresenceCB.setChecked( - default_dict.get('ForcePeakPresence', False)) - - def get(self): - """Return a dictionary of peak search parameters, to be processed by - the :meth:`configure` method of the selected fit theory.""" - ddict = { - 'AutoFwhm': not self.manualFwhmGB.isChecked(), - 'FwhmPoints': self.fwhmPointsSpin.value(), - 'Sensitivity': safe_float(self.sensitivityEntry.text()), - 'AutoScaling': not self.manualScalingGB.isChecked(), - 'Yscaling': safe_float(self.yScalingEntry.text()), - 'ForcePeakPresence': self.forcePeakPresenceCB.isChecked() - } - return ddict - - -class BackgroundPage(qt.QGroupBox): - """Background subtraction configuration, specific to fittheories - estimation functions.""" - def __init__(self, parent=None, - title="Subtract strip background prior to estimation"): - super(BackgroundPage, self).__init__(parent) - self.setTitle(title) - self.setCheckable(True) - self.setToolTip( - "The strip algorithm strips away peaks to compute the " + - "background signal.\nAt each iteration, a sample is compared " + - "to the average of the two samples at a given distance in both" + - " directions,\n and if its value is higher than the average," - "it is replaced by the average.") - - layout = qt.QGridLayout(self) - self.setLayout(layout) - - for i, label_text in enumerate( - ["Strip width (in samples)", - "Number of iterations", - "Strip threshold factor"]): - label = qt.QLabel(label_text) - layout.addWidget(label, i, 0) - - self.stripWidthSpin = qt.QSpinBox(self) - self.stripWidthSpin.setToolTip( - "Width, in number of samples, of the strip operator") - self.stripWidthSpin.setRange(1, 999999) - - layout.addWidget(self.stripWidthSpin, 0, 1) - - self.numIterationsSpin = qt.QSpinBox(self) - self.numIterationsSpin.setToolTip( - "Number of iterations of the strip algorithm") - self.numIterationsSpin.setRange(1, 999999) - layout.addWidget(self.numIterationsSpin, 1, 1) - - self.thresholdFactorEntry = qt.QLineEdit(self) - self.thresholdFactorEntry.setToolTip( - "Factor used by the strip algorithm to decide whether a sample" + - "value should be stripped.\nThe value must be higher than the " + - "average of the 2 samples at +- w times this factor.\n") - self.thresholdFactorEntry.setValidator(qt.QDoubleValidator(self)) - layout.addWidget(self.thresholdFactorEntry, 2, 1) - - self.smoothStripGB = qt.QGroupBox("Apply smoothing prior to strip", self) - self.smoothStripGB.setCheckable(True) - self.smoothStripGB.setToolTip( - "Apply a smoothing before subtracting strip background" + - " in fit and estimate processes") - smoothlayout = qt.QHBoxLayout(self.smoothStripGB) - label = qt.QLabel("Smoothing width (Savitsky-Golay)") - smoothlayout.addWidget(label) - self.smoothingWidthSpin = qt.QSpinBox(self) - self.smoothingWidthSpin.setToolTip( - "Width parameter for Savitsky-Golay smoothing (number of samples, must be odd)") - self.smoothingWidthSpin.setRange(3, 101) - self.smoothingWidthSpin.setSingleStep(2) - smoothlayout.addWidget(self.smoothingWidthSpin) - - layout.addWidget(self.smoothStripGB, 3, 0, 1, 2) - - layout.setRowStretch(4, 1) - - self.setDefault() - - def setDefault(self, default_dict=None): - """Set default values for all widgets. - - :param default_dict: If a default config dictionary is provided as - a parameter, its values are used as default values.""" - if default_dict is None: - default_dict = {} - - self.setChecked( - default_dict.get('StripBackgroundFlag', True)) - - self.stripWidthSpin.setValue( - default_dict.get('StripWidth', 2)) - self.numIterationsSpin.setValue( - default_dict.get('StripIterations', 5000)) - self.thresholdFactorEntry.setText( - str(default_dict.get('StripThreshold', 1.0))) - self.smoothStripGB.setChecked( - default_dict.get('SmoothingFlag', False)) - self.smoothingWidthSpin.setValue( - default_dict.get('SmoothingWidth', 3)) - - def get(self): - """Return a dictionary of background subtraction parameters, to be - processed by the :meth:`configure` method of the selected fit theory. - """ - ddict = { - 'StripBackgroundFlag': self.isChecked(), - 'StripWidth': self.stripWidthSpin.value(), - 'StripIterations': self.numIterationsSpin.value(), - 'StripThreshold': safe_float(self.thresholdFactorEntry.text()), - 'SmoothingFlag': self.smoothStripGB.isChecked(), - 'SmoothingWidth': self.smoothingWidthSpin.value() - } - return ddict - - -def safe_float(string_, default=1.0): - """Convert a string into a float. - If the conversion fails, return the default value. - """ - try: - ret = float(string_) - except ValueError: - return default - else: - return ret - - -def safe_int(string_, default=1): - """Convert a string into a integer. - If the conversion fails, return the default value. - """ - try: - ret = int(float(string_)) - except ValueError: - return default - else: - return ret - - -def getFitConfigDialog(parent=None, default=None, modal=True): - """Instantiate and return a fit configuration dialog, adapted - for configuring standard fit theories from - :mod:`silx.math.fit.fittheories`. - - :return: Instance of :class:`TabsDialogData` with 3 tabs: - :class:`ConstraintsPage`, :class:`SearchPage` and - :class:`BackgroundPage` - """ - tdd = TabsDialogData(parent=parent, default=default) - tdd.addTab(ConstraintsPage(), label="Constraints") - tdd.addTab(SearchPage(), label="Peak search") - tdd.addTab(BackgroundPage(), label="Background") - # apply default to newly added pages - tdd.setDefault() - - return tdd - - -def main(): - a = qt.QApplication([]) - - mw = qt.QMainWindow() - mw.show() - - tdd = getFitConfigDialog(mw, default={"a": 1}) - tdd.show() - tdd.exec_() - print("TabsDialogData result: ", tdd.result()) - print("TabsDialogData output: ", tdd.output) - - a.exec_() - -if __name__ == "__main__": - main() diff --git a/silx/gui/fit/FitWidget.py b/silx/gui/fit/FitWidget.py deleted file mode 100644 index 08731f1..0000000 --- a/silx/gui/fit/FitWidget.py +++ /dev/null @@ -1,739 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2020 European Synchrotron Radiation Facility -# -# This file is part of the PyMca X-ray Fluorescence Toolkit developed at -# the ESRF by the Software group. -# -# 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 provides a widget designed to configure and run a fitting -process with constraints on parameters. - -The main class is :class:`FitWidget`. It relies on -:mod:`silx.math.fit.fitmanager`, which relies on :func:`silx.math.fit.leastsq`. - -The user can choose between functions before running the fit. These function can -be user defined, or by default are loaded from -:mod:`silx.math.fit.fittheories`. -""" - -__authors__ = ["V.A. Sole", "P. Knobel"] -__license__ = "MIT" -__date__ = "17/07/2018" - -import logging -import sys -import traceback - -from silx.math.fit import fittheories -from silx.math.fit import fitmanager, functions -from silx.gui import qt -from .FitWidgets import (FitActionsButtons, FitStatusLines, - FitConfigWidget, ParametersTab) -from .FitConfig import getFitConfigDialog -from .BackgroundWidget import getBgDialog, BackgroundDialog -from ...utils.deprecation import deprecated - -QTVERSION = qt.qVersion() -DEBUG = 0 -_logger = logging.getLogger(__name__) - - -__authors__ = ["V.A. Sole", "P. Knobel"] -__license__ = "MIT" -__date__ = "30/11/2016" - - -class FitWidget(qt.QWidget): - """This widget can be used to configure, run and display results of a - fitting process. - - The standard steps for using this widget is to initialize it, then load - the data to be fitted. - - Optionally, you can also load user defined fit theories. If you skip this - step, a series of default fit functions will be presented (gaussian-like - functions), and you can later load your custom fit theories from an - external file using the GUI. - - A fit theory is a fit function and its associated features: - - - estimation function, - - list of parameter names - - numerical derivative algorithm - - configuration widget - - Once the widget is up and running, the user may select a fit theory and a - background theory, change configuration parameters specific to the theory - run the estimation, set constraints on parameters and run the actual fit. - - The results are displayed in a table. - - .. image:: img/FitWidget.png - """ - sigFitWidgetSignal = qt.Signal(object) - """This signal is emitted by the estimation and fit methods. - It carries a dictionary with two items: - - - *event*: one of the following strings - - - *EstimateStarted*, - - *FitStarted* - - *EstimateFinished*, - - *FitFinished* - - *EstimateFailed* - - *FitFailed* - - - *data*: None, or fit/estimate results (see documentation for - :attr:`silx.math.fit.fitmanager.FitManager.fit_results`) - """ - - def __init__(self, parent=None, title=None, fitmngr=None, - enableconfig=True, enablestatus=True, enablebuttons=True): - """ - - :param parent: Parent widget - :param title: Window title - :param fitmngr: User defined instance of - :class:`silx.math.fit.fitmanager.FitManager`, or ``None`` - :param enableconfig: If ``True``, activate widgets to modify the fit - configuration (select between several fit functions or background - functions, apply global constraints, peak search parameters…) - :param enablestatus: If ``True``, add a fit status widget, to display - a message when fit estimation is available and when fit results - are available, as well as a measure of the fit error. - :param enablebuttons: If ``True``, add buttons to run estimation and - fitting. - """ - if title is None: - title = "FitWidget" - qt.QWidget.__init__(self, parent) - - self.setWindowTitle(title) - layout = qt.QVBoxLayout(self) - - self.fitmanager = self._setFitManager(fitmngr) - """Instance of :class:`FitManager`. - This is the underlying data model of this FitWidget. - - If no custom theories are defined, the default ones from - :mod:`silx.math.fit.fittheories` are imported. - """ - - # reference fitmanager.configure method for direct access - self.configure = self.fitmanager.configure - self.fitconfig = self.fitmanager.fitconfig - - self.configdialogs = {} - """This dictionary defines the fit configuration widgets - associated with the fit theories in :attr:`fitmanager.theories` - - Keys must correspond to existing theory names, i.e. existing keys - in :attr:`fitmanager.theories`. - - Values must be instances of QDialog widgets with an additional - *output* attribute, a dictionary storing configuration parameters - interpreted by the corresponding fit theory. - - The dialog can also define a *setDefault* method to initialize the - widget values with values in a dictionary passed as a parameter. - This will be executed first. - - In case the widget does not actually inherit :class:`QDialog`, it - must at least implement the following methods (executed in this - particular order): - - - :meth:`show`: should cause the widget to become visible to the - user) - - :meth:`exec_`: should run while the user is interacting with the - widget, interrupting the rest of the program. It should - typically end (*return*) when the user clicks an *OK* - or a *Cancel* button. - - :meth:`result`: must return ``True`` if the new configuration in - attribute :attr:`output` is to be accepted (user clicked *OK*), - or return ``False`` if :attr:`output` is to be rejected (user - clicked *Cancel*) - - To associate a custom configuration widget with a fit theory, use - :meth:`associateConfigDialog`. E.g.:: - - fw = FitWidget() - my_config_widget = MyGaussianConfigWidget(parent=fw) - fw.associateConfigDialog(theory_name="Gaussians", - config_widget=my_config_widget) - """ - - self.bgconfigdialogs = {} - """Same as :attr:`configdialogs`, except that the widget is associated - with a background theory in :attr:`fitmanager.bgtheories`""" - - self._associateDefaultConfigDialogs() - - self.guiConfig = None - """Configuration widget at the top of FitWidget, to select - fit function, background function, and open an advanced - configuration dialog.""" - - self.guiParameters = ParametersTab(self) - """Table widget for display of fit parameters and constraints""" - - if enableconfig: - self.guiConfig = FitConfigWidget(self) - """Function selector and configuration widget""" - - self.guiConfig.FunConfigureButton.clicked.connect( - self.__funConfigureGuiSlot) - self.guiConfig.BgConfigureButton.clicked.connect( - self.__bgConfigureGuiSlot) - - self.guiConfig.WeightCheckBox.setChecked( - self.fitconfig.get("WeightFlag", False)) - self.guiConfig.WeightCheckBox.stateChanged[int].connect(self.weightEvent) - - self.guiConfig.BkgComBox.activated[str].connect(self.bkgEvent) - self.guiConfig.FunComBox.activated[str].connect(self.funEvent) - self._populateFunctions() - - layout.addWidget(self.guiConfig) - - layout.addWidget(self.guiParameters) - - if enablestatus: - self.guistatus = FitStatusLines(self) - """Status bar""" - layout.addWidget(self.guistatus) - - if enablebuttons: - self.guibuttons = FitActionsButtons(self) - """Widget with estimate, start fit and dismiss buttons""" - self.guibuttons.EstimateButton.clicked.connect(self.estimate) - self.guibuttons.EstimateButton.setEnabled(False) - self.guibuttons.StartFitButton.clicked.connect(self.startFit) - self.guibuttons.StartFitButton.setEnabled(False) - self.guibuttons.DismissButton.clicked.connect(self.dismiss) - layout.addWidget(self.guibuttons) - - def _setFitManager(self, fitinstance): - """Initialize a :class:`FitManager` instance, to be assigned to - :attr:`fitmanager`, or use a custom FitManager instance. - - :param fitinstance: Existing instance of FitManager, possibly - customized by the user, or None to load a default instance.""" - if isinstance(fitinstance, fitmanager.FitManager): - # customized - fitmngr = fitinstance - else: - # initialize default instance - fitmngr = fitmanager.FitManager() - - # initialize the default fitting functions in case - # none is present - if not len(fitmngr.theories): - fitmngr.loadtheories(fittheories) - - return fitmngr - - def _associateDefaultConfigDialogs(self): - """Fill :attr:`bgconfigdialogs` and :attr:`configdialogs` by calling - :meth:`associateConfigDialog` with default config dialog widgets. - """ - # associate silx.gui.fit.FitConfig with all theories - # Users can later associate their own custom dialogs to - # replace the default. - configdialog = getFitConfigDialog(parent=self, - default=self.fitconfig) - for theory in self.fitmanager.theories: - self.associateConfigDialog(theory, configdialog) - for bgtheory in self.fitmanager.bgtheories: - self.associateConfigDialog(bgtheory, configdialog, - theory_is_background=True) - - # associate silx.gui.fit.BackgroundWidget with Strip and Snip - bgdialog = getBgDialog(parent=self, - default=self.fitconfig) - for bgtheory in ["Strip", "Snip"]: - if bgtheory in self.fitmanager.bgtheories: - self.associateConfigDialog(bgtheory, bgdialog, - theory_is_background=True) - - def _populateFunctions(self): - """Fill combo-boxes with fit theories and background theories - loaded by :attr:`fitmanager`. - Run :meth:`fitmanager.configure` to ensure the custom configuration - of the selected theory has been loaded into :attr:`fitconfig`""" - for theory_name in self.fitmanager.bgtheories: - self.guiConfig.BkgComBox.addItem(theory_name) - self.guiConfig.BkgComBox.setItemData( - self.guiConfig.BkgComBox.findText(theory_name), - self.fitmanager.bgtheories[theory_name].description, - qt.Qt.ToolTipRole) - - for theory_name in self.fitmanager.theories: - self.guiConfig.FunComBox.addItem(theory_name) - self.guiConfig.FunComBox.setItemData( - self.guiConfig.FunComBox.findText(theory_name), - self.fitmanager.theories[theory_name].description, - qt.Qt.ToolTipRole) - - # - activate selected fit theory (if any) - # - activate selected bg theory (if any) - configuration = self.fitmanager.configure() - if self.fitmanager.selectedtheory is None: - # take the first one by default - self.guiConfig.FunComBox.setCurrentIndex(1) - self.funEvent(list(self.fitmanager.theories.keys())[0]) - else: - idx = list(self.fitmanager.theories).index(self.fitmanager.selectedtheory) - self.guiConfig.FunComBox.setCurrentIndex(idx + 1) - self.funEvent(self.fitmanager.selectedtheory) - - if self.fitmanager.selectedbg is None: - self.guiConfig.BkgComBox.setCurrentIndex(1) - self.bkgEvent(list(self.fitmanager.bgtheories.keys())[0]) - else: - idx = list(self.fitmanager.bgtheories).index(self.fitmanager.selectedbg) - self.guiConfig.BkgComBox.setCurrentIndex(idx + 1) - self.bkgEvent(self.fitmanager.selectedbg) - - configuration.update(self.configure()) - - @deprecated(replacement='setData', since_version='0.3.0') - def setdata(self, x, y, sigmay=None, xmin=None, xmax=None): - self.setData(x, y, sigmay, xmin, xmax) - - def setData(self, x=None, y=None, sigmay=None, xmin=None, xmax=None): - """Set data to be fitted. - - :param x: Abscissa data. If ``None``, :attr:`xdata`` is set to - ``numpy.array([0.0, 1.0, 2.0, ..., len(y)-1])`` - :type x: Sequence or numpy array or None - :param y: The dependant data ``y = f(x)``. ``y`` must have the same - shape as ``x`` if ``x`` is not ``None``. - :type y: Sequence or numpy array or None - :param sigmay: The uncertainties in the ``ydata`` array. These are - used as weights in the least-squares problem. - If ``None``, the uncertainties are assumed to be 1. - :type sigmay: Sequence or numpy array or None - :param xmin: Lower value of x values to use for fitting - :param xmax: Upper value of x values to use for fitting - """ - if y is None: - self.guibuttons.EstimateButton.setEnabled(False) - self.guibuttons.StartFitButton.setEnabled(False) - else: - self.guibuttons.EstimateButton.setEnabled(True) - self.guibuttons.StartFitButton.setEnabled(True) - self.fitmanager.setdata(x=x, y=y, sigmay=sigmay, - xmin=xmin, xmax=xmax) - for config_dialog in self.bgconfigdialogs.values(): - if isinstance(config_dialog, BackgroundDialog): - config_dialog.setData(x, y, xmin=xmin, xmax=xmax) - - def associateConfigDialog(self, theory_name, config_widget, - theory_is_background=False): - """Associate an instance of custom configuration dialog widget to - a fit theory or to a background theory. - - This adds or modifies an item in the correspondence table - :attr:`configdialogs` or :attr:`bgconfigdialogs`. - - :param str theory_name: Name of fit theory. This must be a key of dict - :attr:`fitmanager.theories` - :param config_widget: Custom configuration widget. See documentation - for :attr:`configdialogs` - :param bool theory_is_background: If flag is *True*, add dialog to - :attr:`bgconfigdialogs` rather than :attr:`configdialogs` - (default). - :raise: KeyError if parameter ``theory_name`` does not match an - existing fit theory or background theory in :attr:`fitmanager`. - :raise: AttributeError if the widget does not implement the mandatory - methods (*show*, *exec_*, *result*, *setDefault*) or the mandatory - attribute (*output*). - """ - theories = self.fitmanager.bgtheories if theory_is_background else\ - self.fitmanager.theories - - if theory_name not in theories: - raise KeyError("%s does not match an existing fitmanager theory") - - if config_widget is not None: - for mandatory_attr in ["show", "exec_", "result", "output"]: - if not hasattr(config_widget, mandatory_attr): - raise AttributeError( - "Custom configuration widget must define " + - "attribute or method " + mandatory_attr) - - if theory_is_background: - self.bgconfigdialogs[theory_name] = config_widget - else: - self.configdialogs[theory_name] = config_widget - - def _emitSignal(self, ddict): - """Emit pyqtSignal after estimation completed - (``ddict = {'event': 'EstimateFinished', 'data': fit_results}``) - and after fit completed - (``ddict = {'event': 'FitFinished', 'data': fit_results}``)""" - self.sigFitWidgetSignal.emit(ddict) - - def __funConfigureGuiSlot(self): - """Open an advanced configuration dialog widget""" - self.__configureGui(dialog_type="function") - - def __bgConfigureGuiSlot(self): - """Open an advanced configuration dialog widget""" - self.__configureGui(dialog_type="background") - - def __configureGui(self, newconfiguration=None, dialog_type="function"): - """Open an advanced configuration dialog widget to get a configuration - dictionary, or use a supplied configuration dictionary. Call - :meth:`configure` with this dictionary as a parameter. Update the gui - accordingly. Reinitialize the fit results in the table and in - :attr:`fitmanager`. - - :param newconfiguration: User supplied configuration dictionary. If ``None``, - open a dialog widget that returns a dictionary.""" - configuration = self.configure() - # get new dictionary - if newconfiguration is None: - newconfiguration = self.configureDialog(configuration, dialog_type) - # update configuration - configuration.update(self.configure(**newconfiguration)) - # set fit function theory - try: - i = 1 + \ - list(self.fitmanager.theories.keys()).index( - self.fitmanager.selectedtheory) - self.guiConfig.FunComBox.setCurrentIndex(i) - self.funEvent(self.fitmanager.selectedtheory) - except ValueError: - _logger.error("Function not in list %s", - self.fitmanager.selectedtheory) - self.funEvent(list(self.fitmanager.theories.keys())[0]) - # current background - try: - i = 1 + \ - list(self.fitmanager.bgtheories.keys()).index( - self.fitmanager.selectedbg) - self.guiConfig.BkgComBox.setCurrentIndex(i) - self.bkgEvent(self.fitmanager.selectedbg) - except ValueError: - _logger.error("Background not in list %s", - self.fitmanager.selectedbg) - self.bkgEvent(list(self.fitmanager.bgtheories.keys())[0]) - - # update the Gui - self.__initialParameters() - - def configureDialog(self, oldconfiguration, dialog_type="function"): - """Display a dialog, allowing the user to define fit configuration - parameters. - - By default, a common dialog is used for all fit theories. But if the - defined a custom dialog using :meth:`associateConfigDialog`, it is - used instead. - - :param dict oldconfiguration: Dictionary containing previous configuration - :param str dialog_type: "function" or "background" - :return: User defined parameters in a dictionary - """ - newconfiguration = {} - newconfiguration.update(oldconfiguration) - - if dialog_type == "function": - theory = self.fitmanager.selectedtheory - configdialog = self.configdialogs[theory] - elif dialog_type == "background": - theory = self.fitmanager.selectedbg - configdialog = self.bgconfigdialogs[theory] - - # this should only happen if a user specifically associates None - # with a theory, to have no configuration option - if configdialog is None: - return {} - - # update state of configdialog before showing it - if hasattr(configdialog, "setDefault"): - configdialog.setDefault(newconfiguration) - configdialog.show() - configdialog.exec_() - if configdialog.result(): - newconfiguration.update(configdialog.output) - - return newconfiguration - - def estimate(self): - """Run parameter estimation function then emit - :attr:`sigFitWidgetSignal` with a dictionary containing a status - message and a list of fit parameters estimations - in the format defined in - :attr:`silx.math.fit.fitmanager.FitManager.fit_results` - - The emitted dictionary has an *"event"* key that can have - following values: - - - *'EstimateStarted'* - - *'EstimateFailed'* - - *'EstimateFinished'* - """ - try: - theory_name = self.fitmanager.selectedtheory - estimation_function = self.fitmanager.theories[theory_name].estimate - if estimation_function is not None: - ddict = {'event': 'EstimateStarted', - 'data': None} - self._emitSignal(ddict) - self.fitmanager.estimate(callback=self.fitStatus) - else: - msg = qt.QMessageBox(self) - msg.setIcon(qt.QMessageBox.Information) - text = "Function does not define a way to estimate\n" - text += "the initial parameters. Please, fill them\n" - text += "yourself in the table and press Start Fit\n" - msg.setText(text) - msg.setWindowTitle('FitWidget Message') - msg.exec_() - return - except Exception as e: # noqa (we want to catch and report all errors) - _logger.warning('Estimate error: %s', traceback.format_exc()) - msg = qt.QMessageBox(self) - msg.setIcon(qt.QMessageBox.Critical) - msg.setWindowTitle("Estimate Error") - msg.setText("Error on estimate: %s" % e) - msg.exec_() - ddict = { - 'event': 'EstimateFailed', - 'data': None} - self._emitSignal(ddict) - return - - self.guiParameters.fillFromFit( - self.fitmanager.fit_results, view='Fit') - self.guiParameters.removeAllViews(keep='Fit') - ddict = { - 'event': 'EstimateFinished', - 'data': self.fitmanager.fit_results} - self._emitSignal(ddict) - - @deprecated(replacement='startFit', since_version='0.3.0') - def startfit(self): - self.startFit() - - def startFit(self): - """Run fit, then emit :attr:`sigFitWidgetSignal` with a dictionary - containing a status message and a list of fit - parameters results in the format defined in - :attr:`silx.math.fit.fitmanager.FitManager.fit_results` - - The emitted dictionary has an *"event"* key that can have - following values: - - - *'FitStarted'* - - *'FitFailed'* - - *'FitFinished'* - """ - self.fitmanager.fit_results = self.guiParameters.getFitResults() - try: - ddict = {'event': 'FitStarted', - 'data': None} - self._emitSignal(ddict) - self.fitmanager.runfit(callback=self.fitStatus) - except Exception as e: # noqa (we want to catch and report all errors) - _logger.warning('Estimate error: %s', traceback.format_exc()) - msg = qt.QMessageBox(self) - msg.setIcon(qt.QMessageBox.Critical) - msg.setWindowTitle("Fit Error") - msg.setText("Error on Fit: %s" % e) - msg.exec_() - ddict = { - 'event': 'FitFailed', - 'data': None - } - self._emitSignal(ddict) - return - - self.guiParameters.fillFromFit( - self.fitmanager.fit_results, view='Fit') - self.guiParameters.removeAllViews(keep='Fit') - ddict = { - 'event': 'FitFinished', - 'data': self.fitmanager.fit_results - } - self._emitSignal(ddict) - return - - def bkgEvent(self, bgtheory): - """Select background theory, then reinitialize parameters""" - bgtheory = str(bgtheory) - if bgtheory in self.fitmanager.bgtheories: - self.fitmanager.setbackground(bgtheory) - else: - functionsfile = qt.QFileDialog.getOpenFileName( - self, "Select python module with your function(s)", "", - "Python Files (*.py);;All Files (*)") - - if len(functionsfile): - try: - self.fitmanager.loadbgtheories(functionsfile) - except ImportError: - qt.QMessageBox.critical(self, "ERROR", - "Function not imported") - return - else: - # empty the ComboBox - while self.guiConfig.BkgComBox.count() > 1: - self.guiConfig.BkgComBox.removeItem(1) - # and fill it again - for key in self.fitmanager.bgtheories: - self.guiConfig.BkgComBox.addItem(str(key)) - - i = 1 + \ - list(self.fitmanager.bgtheories.keys()).index( - self.fitmanager.selectedbg) - self.guiConfig.BkgComBox.setCurrentIndex(i) - self.__initialParameters() - - def funEvent(self, theoryname): - """Select a fit theory to be used for fitting. If this theory exists - in :attr:`fitmanager`, use it. Then, reinitialize table. - - :param theoryname: Name of the fit theory to use for fitting. If this theory - exists in :attr:`fitmanager`, use it. Else, open a file dialog to open - a custom fit function definition file with - :meth:`fitmanager.loadtheories`. - """ - theoryname = str(theoryname) - if theoryname in self.fitmanager.theories: - self.fitmanager.settheory(theoryname) - else: - # open a load file dialog - functionsfile = qt.QFileDialog.getOpenFileName( - self, "Select python module with your function(s)", "", - "Python Files (*.py);;All Files (*)") - - if len(functionsfile): - try: - self.fitmanager.loadtheories(functionsfile) - except ImportError: - qt.QMessageBox.critical(self, "ERROR", - "Function not imported") - return - else: - # empty the ComboBox - while self.guiConfig.FunComBox.count() > 1: - self.guiConfig.FunComBox.removeItem(1) - # and fill it again - for key in self.fitmanager.theories: - self.guiConfig.FunComBox.addItem(str(key)) - - i = 1 + \ - list(self.fitmanager.theories.keys()).index( - self.fitmanager.selectedtheory) - self.guiConfig.FunComBox.setCurrentIndex(i) - self.__initialParameters() - - def weightEvent(self, flag): - """This is called when WeightCheckBox is clicked, to configure the - *WeightFlag* field in :attr:`fitmanager.fitconfig` and set weights - in the least-square problem.""" - self.configure(WeightFlag=flag) - if flag: - self.fitmanager.enableweight() - else: - # set weights back to 1 - self.fitmanager.disableweight() - - def __initialParameters(self): - """Fill the fit parameters names with names of the parameters of - the selected background theory and the selected fit theory. - Initialize :attr:`fitmanager.fit_results` with these names, and - initialize the table with them. This creates a view called "Fit" - in :attr:`guiParameters`""" - self.fitmanager.parameter_names = [] - self.fitmanager.fit_results = [] - for pname in self.fitmanager.bgtheories[self.fitmanager.selectedbg].parameters: - self.fitmanager.parameter_names.append(pname) - self.fitmanager.fit_results.append({'name': pname, - 'estimation': 0, - 'group': 0, - 'code': 'FREE', - 'cons1': 0, - 'cons2': 0, - 'fitresult': 0.0, - 'sigma': 0.0, - 'xmin': None, - 'xmax': None}) - if self.fitmanager.selectedtheory is not None: - theory = self.fitmanager.selectedtheory - for pname in self.fitmanager.theories[theory].parameters: - self.fitmanager.parameter_names.append(pname + "1") - self.fitmanager.fit_results.append({'name': pname + "1", - 'estimation': 0, - 'group': 1, - 'code': 'FREE', - 'cons1': 0, - 'cons2': 0, - 'fitresult': 0.0, - 'sigma': 0.0, - 'xmin': None, - 'xmax': None}) - - self.guiParameters.fillFromFit( - self.fitmanager.fit_results, view='Fit') - - def fitStatus(self, data): - """Set *status* and *chisq* in status bar""" - if 'chisq' in data: - if data['chisq'] is None: - self.guistatus.ChisqLine.setText(" ") - else: - chisq = data['chisq'] - self.guistatus.ChisqLine.setText("%6.2f" % chisq) - - if 'status' in data: - status = data['status'] - self.guistatus.StatusLine.setText(str(status)) - - def dismiss(self): - """Close FitWidget""" - self.close() - - -if __name__ == "__main__": - import numpy - - x = numpy.arange(1500).astype(numpy.float64) - constant_bg = 3.14 - - p = [1000, 100., 30.0, - 500, 300., 25., - 1700, 500., 35., - 750, 700., 30.0, - 1234, 900., 29.5, - 302, 1100., 30.5, - 75, 1300., 21.] - y = functions.sum_gauss(x, *p) + constant_bg - - a = qt.QApplication(sys.argv) - w = FitWidget() - w.setData(x=x, y=y) - w.show() - a.exec_() diff --git a/silx/gui/fit/FitWidgets.py b/silx/gui/fit/FitWidgets.py deleted file mode 100644 index 408666b..0000000 --- a/silx/gui/fit/FitWidgets.py +++ /dev/null @@ -1,559 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# Copyright (C) 2004-2016 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. -# -# ######################################################################### */ -"""Collection of widgets used to build -:class:`silx.gui.fit.FitWidget.FitWidget`""" - -from collections import OrderedDict - -from silx.gui import qt -from silx.gui.fit.Parameters import Parameters - -QTVERSION = qt.qVersion() - -__authors__ = ["V.A. Sole", "P. Knobel"] -__license__ = "MIT" -__date__ = "13/10/2016" - - -class FitActionsButtons(qt.QWidget): - """Widget with 3 ``QPushButton``: - - The buttons can be accessed as public attributes:: - - - ``EstimateButton`` - - ``StartFitButton`` - - ``DismissButton`` - - You will typically need to access these attributes to connect the buttons - to actions. For instance, if you have 3 functions ``estimate``, - ``runfit`` and ``dismiss``, you can connect them like this:: - - >>> fit_actions_buttons = FitActionsButtons() - >>> fit_actions_buttons.EstimateButton.clicked.connect(estimate) - >>> fit_actions_buttons.StartFitButton.clicked.connect(runfit) - >>> fit_actions_buttons.DismissButton.clicked.connect(dismiss) - - """ - - def __init__(self, parent=None): - qt.QWidget.__init__(self, parent) - - self.resize(234, 53) - - grid_layout = qt.QGridLayout(self) - grid_layout.setContentsMargins(11, 11, 11, 11) - grid_layout.setSpacing(6) - layout = qt.QHBoxLayout(None) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(6) - - self.EstimateButton = qt.QPushButton(self) - self.EstimateButton.setText("Estimate") - layout.addWidget(self.EstimateButton) - spacer = qt.QSpacerItem(20, 20, - qt.QSizePolicy.Expanding, - qt.QSizePolicy.Minimum) - layout.addItem(spacer) - - self.StartFitButton = qt.QPushButton(self) - self.StartFitButton.setText("Start Fit") - layout.addWidget(self.StartFitButton) - spacer_2 = qt.QSpacerItem(20, 20, - qt.QSizePolicy.Expanding, - qt.QSizePolicy.Minimum) - layout.addItem(spacer_2) - - self.DismissButton = qt.QPushButton(self) - self.DismissButton.setText("Dismiss") - layout.addWidget(self.DismissButton) - - grid_layout.addLayout(layout, 0, 0) - - -class FitStatusLines(qt.QWidget): - """Widget with 2 greyed out write-only ``QLineEdit``. - - These text widgets can be accessed as public attributes:: - - - ``StatusLine`` - - ``ChisqLine`` - - You will typically need to access these widgets to update the displayed - text:: - - >>> fit_status_lines = FitStatusLines() - >>> fit_status_lines.StatusLine.setText("Ready") - >>> fit_status_lines.ChisqLine.setText("%6.2f" % 0.01) - - """ - - def __init__(self, parent=None): - qt.QWidget.__init__(self, parent) - - self.resize(535, 47) - - layout = qt.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(6) - - self.StatusLabel = qt.QLabel(self) - self.StatusLabel.setText("Status:") - layout.addWidget(self.StatusLabel) - - self.StatusLine = qt.QLineEdit(self) - self.StatusLine.setText("Ready") - self.StatusLine.setReadOnly(1) - layout.addWidget(self.StatusLine) - - self.ChisqLabel = qt.QLabel(self) - self.ChisqLabel.setText("Reduced chisq:") - layout.addWidget(self.ChisqLabel) - - self.ChisqLine = qt.QLineEdit(self) - self.ChisqLine.setMaximumSize(qt.QSize(16000, 32767)) - self.ChisqLine.setText("") - self.ChisqLine.setReadOnly(1) - layout.addWidget(self.ChisqLine) - - -class FitConfigWidget(qt.QWidget): - """Widget whose purpose is to select a fit theory and a background - theory, load a new fit theory definition file and provide - a "Configure" button to open an advanced configuration dialog. - - This is used in :class:`silx.gui.fit.FitWidget.FitWidget`, to offer - an interface to quickly modify the main parameters prior to running a fit: - - - select a fitting function through :attr:`FunComBox` - - select a background function through :attr:`BkgComBox` - - open a dialog for modifying advanced parameters through - :attr:`FunConfigureButton` - """ - def __init__(self, parent=None): - qt.QWidget.__init__(self, parent) - - self.setWindowTitle("FitConfigGUI") - - layout = qt.QGridLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(6) - - self.FunLabel = qt.QLabel(self) - self.FunLabel.setText("Function") - layout.addWidget(self.FunLabel, 0, 0) - - self.FunComBox = qt.QComboBox(self) - self.FunComBox.addItem("Add Function(s)") - self.FunComBox.setItemData(self.FunComBox.findText("Add Function(s)"), - "Load fit theories from a file", - qt.Qt.ToolTipRole) - layout.addWidget(self.FunComBox, 0, 1) - - self.BkgLabel = qt.QLabel(self) - self.BkgLabel.setText("Background") - layout.addWidget(self.BkgLabel, 1, 0) - - self.BkgComBox = qt.QComboBox(self) - self.BkgComBox.addItem("Add Background(s)") - self.BkgComBox.setItemData(self.BkgComBox.findText("Add Background(s)"), - "Load background theories from a file", - qt.Qt.ToolTipRole) - layout.addWidget(self.BkgComBox, 1, 1) - - self.FunConfigureButton = qt.QPushButton(self) - self.FunConfigureButton.setText("Configure") - self.FunConfigureButton.setToolTip( - "Open a configuration dialog for the selected function") - layout.addWidget(self.FunConfigureButton, 0, 2) - - self.BgConfigureButton = qt.QPushButton(self) - self.BgConfigureButton.setText("Configure") - self.BgConfigureButton.setToolTip( - "Open a configuration dialog for the selected background") - layout.addWidget(self.BgConfigureButton, 1, 2) - - self.WeightCheckBox = qt.QCheckBox(self) - self.WeightCheckBox.setText("Weighted fit") - self.WeightCheckBox.setToolTip( - "Enable usage of weights in the least-square problem.\n Use" + - " the uncertainties (sigma) if provided, else use sqrt(y).") - - layout.addWidget(self.WeightCheckBox, 0, 3, 2, 1) - - layout.setColumnStretch(4, 1) - - -class ParametersTab(qt.QTabWidget): - """This widget provides tabs to display and modify fit parameters. Each - tab contains a table with fit data such as parameter names, estimated - values, fit constraints, and final fit results. - - The usual way to initialize the table is to fill it with the fit - parameters from a :class:`silx.math.fit.fitmanager.FitManager` object, after - the estimation process or after the final fit. - - In the following example we use a :class:`ParametersTab` to display the - results of two separate fits:: - - from silx.math.fit import fittheories - from silx.math.fit import fitmanager - from silx.math.fit import functions - from silx.gui import qt - import numpy - - a = qt.QApplication([]) - - # Create synthetic data - x = numpy.arange(1000) - y1 = functions.sum_gauss(x, 100, 400, 100) - - fit = fitmanager.FitManager(x=x, y=y1) - - fitfuns = fittheories.FitTheories() - fit.addtheory(theory="Gaussian", - function=functions.sum_gauss, - parameters=("height", "peak center", "fwhm"), - estimate=fitfuns.estimate_height_position_fwhm) - fit.settheory('Gaussian') - fit.configure(PositiveFwhmFlag=True, - PositiveHeightAreaFlag=True, - AutoFwhm=True,) - - # Fit - fit.estimate() - fit.runfit() - - # Show first fit result in a tab in our widget - w = ParametersTab() - w.show() - w.fillFromFit(fit.fit_results, view='Gaussians') - - # new synthetic data - y2 = functions.sum_splitgauss(x, - 100, 400, 100, 40, - 10, 600, 50, 500, - 80, 850, 10, 50) - fit.setData(x=x, y=y2) - - # Define new theory - fit.addtheory(theory="Asymetric gaussian", - function=functions.sum_splitgauss, - parameters=("height", "peak center", "left fwhm", "right fwhm"), - estimate=fitfuns.estimate_splitgauss) - fit.settheory('Asymetric gaussian') - - # Fit - fit.estimate() - fit.runfit() - - # Show first fit result in another tab in our widget - w.fillFromFit(fit.fit_results, view='Asymetric gaussians') - a.exec_() - - """ - - def __init__(self, parent=None, name="FitParameters"): - """ - - :param parent: Parent widget - :param name: Widget title - """ - qt.QTabWidget.__init__(self, parent) - self.setWindowTitle(name) - self.setContentsMargins(0, 0, 0, 0) - - self.views = OrderedDict() - """Dictionary of views. Keys are view names, - items are :class:`Parameters` widgets""" - - self.latest_view = None - """Name of latest view""" - - # the widgets/tables themselves - self.tables = {} - """Dictionary of :class:`silx.gui.fit.parameters.Parameters` objects. - These objects store fit results - """ - - self.setContentsMargins(10, 10, 10, 10) - - def setView(self, view=None, fitresults=None): - """Add or update a table. Fill it with data from a fit - - :param view: Tab name to be added or updated. If ``None``, use the - latest view. - :param fitresults: Fit data to be added to the table - :raise: KeyError if no view name specified and no latest view - available. - """ - if view is None: - if self.latest_view is not None: - view = self.latest_view - else: - raise KeyError( - "No view available. You must specify a view" + - " name the first time you call this method." - ) - - if view in self.tables.keys(): - table = self.tables[view] - else: - # create the parameters instance - self.tables[view] = Parameters(self) - table = self.tables[view] - self.views[view] = table - self.addTab(table, str(view)) - - if fitresults is not None: - table.fillFromFit(fitresults) - - self.setCurrentWidget(self.views[view]) - self.latest_view = view - - def renameView(self, oldname=None, newname=None): - """Rename a view (tab) - - :param oldname: Name of the view to be renamed - :param newname: New name of the view""" - error = 1 - if newname is not None: - if newname not in self.views.keys(): - if oldname in self.views.keys(): - parameterlist = self.tables[oldname].getFitResults() - self.setView(view=newname, fitresults=parameterlist) - self.removeView(oldname) - error = 0 - return error - - def fillFromFit(self, fitparameterslist, view=None): - """Update a view with data from a fit (alias for :meth:`setView`) - - :param view: Tab name to be added or updated (default: latest view) - :param fitparameterslist: Fit data to be added to the table - """ - self.setView(view=view, fitresults=fitparameterslist) - - def getFitResults(self, name=None): - """Call :meth:`getFitResults` for the - :class:`silx.gui.fit.parameters.Parameters` corresponding to the - latest table or to the named table (if ``name`` is not - ``None``). This return a list of dictionaries in the format used by - :class:`silx.math.fit.fitmanager.FitManager` to store fit parameter - results. - - :param name: View name. - """ - if name is None: - name = self.latest_view - return self.tables[name].getFitResults() - - def removeView(self, name): - """Remove a view by name. - - :param name: View name. - """ - if name in self.views: - index = self.indexOf(self.tables[name]) - self.removeTab(index) - index = self.indexOf(self.views[name]) - self.removeTab(index) - del self.tables[name] - del self.views[name] - - def removeAllViews(self, keep=None): - """Remove all views, except the one specified (argument - ``keep``) - - :param keep: Name of the view to be kept.""" - for view in self.tables: - if view != keep: - self.removeView(view) - - def getHtmlText(self, name=None): - """Return the table data as HTML - - :param name: View name.""" - if name is None: - name = self.latest_view - table = self.tables[name] - lemon = ("#%x%x%x" % (255, 250, 205)).upper() - hcolor = ("#%x%x%x" % (230, 240, 249)).upper() - text = "" - text += "<nobr>" - text += "<table>" - text += "<tr>" - ncols = table.columnCount() - for l in range(ncols): - text += ('<td align="left" bgcolor="%s"><b>' % hcolor) - if QTVERSION < '4.0.0': - text += (str(table.horizontalHeader().label(l))) - else: - text += (str(table.horizontalHeaderItem(l).text())) - text += "</b></td>" - text += "</tr>" - nrows = table.rowCount() - for r in range(nrows): - text += "<tr>" - item = table.item(r, 0) - newtext = "" - if item is not None: - newtext = str(item.text()) - if len(newtext): - color = "white" - b = "<b>" - else: - b = "" - color = lemon - try: - # MyQTable item has color defined - cc = table.item(r, 0).color - cc = ("#%x%x%x" % (cc.red(), cc.green(), cc.blue())).upper() - color = cc - except: - pass - for c in range(ncols): - item = table.item(r, c) - newtext = "" - if item is not None: - newtext = str(item.text()) - if len(newtext): - finalcolor = color - else: - finalcolor = "white" - if c < 2: - text += ('<td align="left" bgcolor="%s">%s' % - (finalcolor, b)) - else: - text += ('<td align="right" bgcolor="%s">%s' % - (finalcolor, b)) - text += newtext - if len(b): - text += "</td>" - else: - text += "</b></td>" - item = table.item(r, 0) - newtext = "" - if item is not None: - newtext = str(item.text()) - if len(newtext): - text += "</b>" - text += "</tr>" - text += "\n" - text += "</table>" - text += "</nobr>" - return text - - def getText(self, name=None): - """Return the table data as CSV formatted text, using tabulation - characters as separators. - - :param name: View name.""" - if name is None: - name = self.latest_view - table = self.tables[name] - text = "" - ncols = table.columnCount() - for l in range(ncols): - text += (str(table.horizontalHeaderItem(l).text())) + "\t" - text += "\n" - nrows = table.rowCount() - for r in range(nrows): - for c in range(ncols): - newtext = "" - if c != 4: - item = table.item(r, c) - if item is not None: - newtext = str(item.text()) - else: - item = table.cellWidget(r, c) - if item is not None: - newtext = str(item.currentText()) - text += newtext + "\t" - text += "\n" - text += "\n" - return text - - -def test(): - from silx.math.fit import fittheories - from silx.math.fit import fitmanager - from silx.math.fit import functions - from silx.gui.plot.PlotWindow import PlotWindow - import numpy - - a = qt.QApplication([]) - - x = numpy.arange(1000) - y1 = functions.sum_gauss(x, 100, 400, 100) - - fit = fitmanager.FitManager(x=x, y=y1) - - fitfuns = fittheories.FitTheories() - fit.addtheory(name="Gaussian", - function=functions.sum_gauss, - parameters=("height", "peak center", "fwhm"), - estimate=fitfuns.estimate_height_position_fwhm) - fit.settheory('Gaussian') - fit.configure(PositiveFwhmFlag=True, - PositiveHeightAreaFlag=True, - AutoFwhm=True,) - - # Fit - fit.estimate() - fit.runfit() - - w = ParametersTab() - w.show() - w.fillFromFit(fit.fit_results, view='Gaussians') - - y2 = functions.sum_splitgauss(x, - 100, 400, 100, 40, - 10, 600, 50, 500, - 80, 850, 10, 50) - fit.setdata(x=x, y=y2) - - # Define new theory - fit.addtheory(name="Asymetric gaussian", - function=functions.sum_splitgauss, - parameters=("height", "peak center", "left fwhm", "right fwhm"), - estimate=fitfuns.estimate_splitgauss) - fit.settheory('Asymetric gaussian') - - # Fit - fit.estimate() - fit.runfit() - - w.fillFromFit(fit.fit_results, view='Asymetric gaussians') - - # Plot - pw = PlotWindow(control=True) - pw.addCurve(x, y1, "Gaussians") - pw.addCurve(x, y2, "Asymetric gaussians") - pw.show() - - a.exec_() - - -if __name__ == "__main__": - test() diff --git a/silx/gui/fit/Parameters.py b/silx/gui/fit/Parameters.py deleted file mode 100644 index 62e3278..0000000 --- a/silx/gui/fit/Parameters.py +++ /dev/null @@ -1,882 +0,0 @@ -# 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) diff --git a/silx/gui/fit/__init__.py b/silx/gui/fit/__init__.py deleted file mode 100644 index e4fd3ab..0000000 --- a/silx/gui/fit/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# Copyright (C) 2016 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. -# -# ############################################################################*/ -__authors__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "07/07/2016" - -from .FitWidget import FitWidget diff --git a/silx/gui/fit/setup.py b/silx/gui/fit/setup.py deleted file mode 100644 index 6672363..0000000 --- a/silx/gui/fit/setup.py +++ /dev/null @@ -1,43 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016 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. -# -# ###########################################################################*/ -__authors__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "21/07/2016" - - -from numpy.distutils.misc_util import Configuration - - -def configuration(parent_package='', top_path=None): - config = Configuration('fit', parent_package, top_path) - config.add_subpackage('test') - - return config - - -if __name__ == "__main__": - from numpy.distutils.core import setup - - setup(configuration=configuration) diff --git a/silx/gui/fit/test/__init__.py b/silx/gui/fit/test/__init__.py deleted file mode 100644 index 2236d64..0000000 --- a/silx/gui/fit/test/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016 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. -# -# ###########################################################################*/ -import unittest - -from .testFitWidget import suite as testFitWidgetSuite -from .testFitConfig import suite as testFitConfigSuite -from .testBackgroundWidget import suite as testBackgroundWidgetSuite - - -__authors__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "21/07/2016" - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTests( - [testFitWidgetSuite(), - testFitConfigSuite(), - testBackgroundWidgetSuite()]) - return test_suite diff --git a/silx/gui/fit/test/testBackgroundWidget.py b/silx/gui/fit/test/testBackgroundWidget.py deleted file mode 100644 index 03b17b9..0000000 --- a/silx/gui/fit/test/testBackgroundWidget.py +++ /dev/null @@ -1,83 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016 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. -# -# ###########################################################################*/ -import unittest - -from silx.gui.utils.testutils import TestCaseQt - -from .. import BackgroundWidget - -__authors__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "05/12/2016" - - -class TestBackgroundWidget(TestCaseQt): - def setUp(self): - super(TestBackgroundWidget, self).setUp() - self.bgdialog = BackgroundWidget.BackgroundDialog() - self.bgdialog.setData(list([0, 1, 2, 3]), - list([0, 1, 4, 8])) - self.qWaitForWindowExposed(self.bgdialog) - - def tearDown(self): - del self.bgdialog - super(TestBackgroundWidget, self).tearDown() - - def testShow(self): - self.bgdialog.show() - self.bgdialog.hide() - - def testAccept(self): - self.bgdialog.accept() - self.assertTrue(self.bgdialog.result()) - - def testReject(self): - self.bgdialog.reject() - self.assertFalse(self.bgdialog.result()) - - def testDefaultOutput(self): - self.bgdialog.accept() - output = self.bgdialog.output - - for key in ["algorithm", "StripThreshold", "SnipWidth", - "StripIterations", "StripWidth", "SmoothingFlag", - "SmoothingWidth", "AnchorsFlag", "AnchorsList"]: - self.assertIn(key, output) - - self.assertFalse(output["AnchorsFlag"]) - self.assertEqual(output["StripWidth"], 1) - self.assertEqual(output["SmoothingFlag"], False) - self.assertEqual(output["SmoothingWidth"], 3) - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestBackgroundWidget)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/fit/test/testFitConfig.py b/silx/gui/fit/test/testFitConfig.py deleted file mode 100644 index f89c099..0000000 --- a/silx/gui/fit/test/testFitConfig.py +++ /dev/null @@ -1,95 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016 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. -# -# ###########################################################################*/ -"""Basic tests for :class:`FitConfig`""" - -__authors__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "05/12/2016" - -import unittest - -from silx.gui.utils.testutils import TestCaseQt -from .. import FitConfig - - -class TestFitConfig(TestCaseQt): - """Basic test for FitWidget""" - - def setUp(self): - super(TestFitConfig, self).setUp() - self.fit_config = FitConfig.getFitConfigDialog(modal=False) - self.qWaitForWindowExposed(self.fit_config) - - def tearDown(self): - del self.fit_config - super(TestFitConfig, self).tearDown() - - def testShow(self): - self.fit_config.show() - self.fit_config.hide() - - def testAccept(self): - self.fit_config.accept() - self.assertTrue(self.fit_config.result()) - - def testReject(self): - self.fit_config.reject() - self.assertFalse(self.fit_config.result()) - - def testDefaultOutput(self): - self.fit_config.accept() - output = self.fit_config.output - - for key in ["AutoFwhm", - "PositiveHeightAreaFlag", - "QuotedPositionFlag", - "PositiveFwhmFlag", - "SameFwhmFlag", - "QuotedEtaFlag", - "NoConstraintsFlag", - "FwhmPoints", - "Sensitivity", - "Yscaling", - "ForcePeakPresence", - "StripBackgroundFlag", - "StripWidth", - "StripIterations", - "StripThreshold", - "SmoothingFlag"]: - self.assertIn(key, output) - - self.assertTrue(output["AutoFwhm"]) - self.assertEqual(output["StripWidth"], 2) - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestFitConfig)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/fit/test/testFitWidget.py b/silx/gui/fit/test/testFitWidget.py deleted file mode 100644 index cfd2bc9..0000000 --- a/silx/gui/fit/test/testFitWidget.py +++ /dev/null @@ -1,135 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016 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. -# -# ###########################################################################*/ -"""Basic tests for :class:`FitWidget`""" - -import unittest - -from silx.gui.utils.testutils import TestCaseQt - -from ... import qt -from .. import FitWidget - -from ....math.fit.fittheory import FitTheory -from ....math.fit.fitmanager import FitManager - -__authors__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "05/12/2016" - - -class TestFitWidget(TestCaseQt): - """Basic test for FitWidget""" - - def setUp(self): - super(TestFitWidget, self).setUp() - self.fit_widget = FitWidget() - self.fit_widget.show() - self.qWaitForWindowExposed(self.fit_widget) - - def tearDown(self): - self.fit_widget.setAttribute(qt.Qt.WA_DeleteOnClose) - self.fit_widget.close() - del self.fit_widget - super(TestFitWidget, self).tearDown() - - def testShow(self): - pass - - def testInteract(self): - self.mouseClick(self.fit_widget, qt.Qt.LeftButton) - self.keyClick(self.fit_widget, qt.Qt.Key_Enter) - self.qapp.processEvents() - - def testCustomConfigWidget(self): - class CustomConfigWidget(qt.QDialog): - def __init__(self): - qt.QDialog.__init__(self) - self.setModal(True) - self.ok = qt.QPushButton("ok", self) - self.ok.clicked.connect(self.accept) - cancel = qt.QPushButton("cancel", self) - cancel.clicked.connect(self.reject) - layout = qt.QVBoxLayout(self) - layout.addWidget(self.ok) - layout.addWidget(cancel) - self.output = {"hello": "world"} - - def fitfun(x, a, b): - return a * x + b - - x = list(range(0, 100)) - y = [fitfun(x_, 2, 3) for x_ in x] - - def conf(**kw): - return {"spam": "eggs", - "hello": "world!"} - - theory = FitTheory( - function=fitfun, - parameters=["a", "b"], - configure=conf) - - fitmngr = FitManager() - fitmngr.setdata(x, y) - fitmngr.addtheory("foo", theory) - fitmngr.addtheory("bar", theory) - fitmngr.addbgtheory("spam", theory) - - fw = FitWidget(fitmngr=fitmngr) - fw.associateConfigDialog("spam", CustomConfigWidget(), - theory_is_background=True) - fw.associateConfigDialog("foo", CustomConfigWidget()) - fw.show() - self.qWaitForWindowExposed(fw) - - fw.bgconfigdialogs["spam"].accept() - self.assertTrue(fw.bgconfigdialogs["spam"].result()) - - self.assertEqual(fw.bgconfigdialogs["spam"].output, - {"hello": "world"}) - - fw.bgconfigdialogs["spam"].reject() - self.assertFalse(fw.bgconfigdialogs["spam"].result()) - - fw.configdialogs["foo"].accept() - self.assertTrue(fw.configdialogs["foo"].result()) - - # todo: figure out how to click fw.configdialog.ok to close dialog - # open dialog - # self.mouseClick(fw.guiConfig.FunConfigureButton, qt.Qt.LeftButton) - # clove dialog - # self.mouseClick(fw.configdialogs["foo"].ok, qt.Qt.LeftButton) - # self.qapp.processEvents() - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestFitWidget)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') |