From f7bdc2acff3c13a6d632c28c4569690ab106eed7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Picca=20Fr=C3=A9d=C3=A9ric-Emmanuel?= Date: Fri, 18 Aug 2017 14:48:52 +0200 Subject: Import Upstream version 0.5.0+dfsg --- silx/gui/fit/FitConfig.py | 540 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 540 insertions(+) create mode 100644 silx/gui/fit/FitConfig.py (limited to 'silx/gui/fit/FitConfig.py') 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() -- cgit v1.2.3