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