From 28c15ff565ee2c070e80dd443a12418522c40007 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sun, 12 Mar 2023 19:37:10 +0000 Subject: [PATCH] extrapolate_fit (#21) New sets with arbitrary x range can be created from fit results Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/21 --- LICE | 13 -- src/gui_qt/_py/fitresult.py | 122 +++++++++++------ src/gui_qt/data/container.py | 6 +- src/gui_qt/data/datawidget/datawidget.py | 13 +- src/gui_qt/fit/result.py | 85 ++++++++++-- src/gui_qt/main/mainwindow.py | 22 ++- src/gui_qt/main/management.py | 66 +++++++-- src/nmreval/fit/model.py | 4 +- src/nmreval/fit/result.py | 66 ++++++--- src/resources/_ui/fitresult.ui | 165 ++++++++++++++--------- 10 files changed, 393 insertions(+), 169 deletions(-) delete mode 100644 LICE diff --git a/LICE b/LICE deleted file mode 100644 index c773af1..0000000 --- a/LICE +++ /dev/null @@ -1,13 +0,0 @@ -BSD 3-Clause License - -Copyright (c) 2023 Dominik Demuth. - -Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: - -1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. - -2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. - -3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/gui_qt/_py/fitresult.py b/src/gui_qt/_py/fitresult.py index 2634065..987e4c6 100644 --- a/src/gui_qt/_py/fitresult.py +++ b/src/gui_qt/_py/fitresult.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file './resources/_ui/fitresult.ui' +# Form implementation generated from reading ui file '/autohome/dominik/nmreval-gitea/src/resources/_ui/fitresult.ui' # -# Created by: PyQt5 UI code generator 5.15.4 +# Created by: PyQt5 UI code generator 5.15.7 # # WARNING: Any manual changes made to this file will be lost when pyuic5 is # run again. Do not edit this file unless you know what you are doing. @@ -14,7 +14,7 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_Dialog(object): def setupUi(self, Dialog): Dialog.setObjectName("Dialog") - Dialog.resize(817, 584) + Dialog.resize(864, 649) self.gridLayout = QtWidgets.QGridLayout(Dialog) self.gridLayout.setObjectName("gridLayout") self.sets_comboBox = ElideComboBox(Dialog) @@ -51,6 +51,21 @@ class Ui_Dialog(object): self.gridLayout_2.setContentsMargins(3, 3, 3, 3) self.gridLayout_2.setSpacing(3) self.gridLayout_2.setObjectName("gridLayout_2") + self.extrapolate_box = QtWidgets.QCheckBox(self.groupBox) + self.extrapolate_box.setObjectName("extrapolate_box") + self.gridLayout_2.addWidget(self.extrapolate_box, 1, 0, 1, 1) + self.parameter_checkbox = QtWidgets.QCheckBox(self.groupBox) + self.parameter_checkbox.setObjectName("parameter_checkbox") + self.gridLayout_2.addWidget(self.parameter_checkbox, 0, 5, 1, 1) + self.graph_comboBox = QtWidgets.QComboBox(self.groupBox) + self.graph_comboBox.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.graph_comboBox.sizePolicy().hasHeightForWidth()) + self.graph_comboBox.setSizePolicy(sizePolicy) + self.graph_comboBox.setObjectName("graph_comboBox") + self.gridLayout_2.addWidget(self.graph_comboBox, 1, 6, 1, 1) self.graph_checkBox = QtWidgets.QCheckBox(self.groupBox) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -59,22 +74,44 @@ class Ui_Dialog(object): self.graph_checkBox.setSizePolicy(sizePolicy) self.graph_checkBox.setChecked(True) self.graph_checkBox.setObjectName("graph_checkBox") - self.gridLayout_2.addWidget(self.graph_checkBox, 1, 1, 1, 1) - self.graph_comboBox = QtWidgets.QComboBox(self.groupBox) - self.graph_comboBox.setEnabled(False) - self.graph_comboBox.setObjectName("graph_comboBox") - self.gridLayout_2.addWidget(self.graph_comboBox, 1, 2, 1, 1) + self.gridLayout_2.addWidget(self.graph_checkBox, 1, 5, 1, 1) + self.minx_line = QtWidgets.QLineEdit(self.groupBox) + self.minx_line.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.minx_line.sizePolicy().hasHeightForWidth()) + self.minx_line.setSizePolicy(sizePolicy) + self.minx_line.setObjectName("minx_line") + self.gridLayout_2.addWidget(self.minx_line, 1, 1, 1, 1) + self.line_2 = QtWidgets.QFrame(self.groupBox) + self.line_2.setFrameShape(QtWidgets.QFrame.VLine) + self.line_2.setFrameShadow(QtWidgets.QFrame.Sunken) + self.line_2.setObjectName("line_2") + self.gridLayout_2.addWidget(self.line_2, 0, 4, 2, 1) + self.maxx_line = QtWidgets.QLineEdit(self.groupBox) + self.maxx_line.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.maxx_line.sizePolicy().hasHeightForWidth()) + self.maxx_line.setSizePolicy(sizePolicy) + self.maxx_line.setObjectName("maxx_line") + self.gridLayout_2.addWidget(self.maxx_line, 1, 2, 1, 1) + self.numx_line = QtWidgets.QLineEdit(self.groupBox) + self.numx_line.setEnabled(False) + self.numx_line.setObjectName("numx_line") + self.gridLayout_2.addWidget(self.numx_line, 1, 3, 1, 1) + self.horizontalLayout = QtWidgets.QHBoxLayout() + self.horizontalLayout.setObjectName("horizontalLayout") self.curve_checkbox = QtWidgets.QCheckBox(self.groupBox) self.curve_checkbox.setChecked(True) self.curve_checkbox.setObjectName("curve_checkbox") - self.gridLayout_2.addWidget(self.curve_checkbox, 0, 0, 1, 1) + self.horizontalLayout.addWidget(self.curve_checkbox) self.partial_checkBox = QtWidgets.QCheckBox(self.groupBox) self.partial_checkBox.setObjectName("partial_checkBox") - self.gridLayout_2.addWidget(self.partial_checkBox, 1, 0, 1, 1) - self.parameter_checkbox = QtWidgets.QCheckBox(self.groupBox) - self.parameter_checkbox.setChecked(True) - self.parameter_checkbox.setObjectName("parameter_checkbox") - self.gridLayout_2.addWidget(self.parameter_checkbox, 0, 1, 1, 1) + self.horizontalLayout.addWidget(self.partial_checkBox) + self.gridLayout_2.addLayout(self.horizontalLayout, 0, 0, 1, 4) self.gridLayout.addWidget(self.groupBox, 5, 0, 1, 2) self.horizontalLayout_2 = QtWidgets.QHBoxLayout() self.horizontalLayout_2.setSpacing(3) @@ -91,40 +128,38 @@ class Ui_Dialog(object): self.line.setFrameShadow(QtWidgets.QFrame.Sunken) self.line.setObjectName("line") self.gridLayout.addWidget(self.line, 3, 0, 1, 2) - self.stack = QtWidgets.QToolBox(Dialog) + self.stack = QtWidgets.QTabWidget(Dialog) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.stack.sizePolicy().hasHeightForWidth()) self.stack.setSizePolicy(sizePolicy) self.stack.setObjectName("stack") - self.page = QtWidgets.QWidget() - self.page.setGeometry(QtCore.QRect(0, 0, 399, 346)) - self.page.setObjectName("page") - self.gridLayout_3 = QtWidgets.QGridLayout(self.page) + self.stackPage1 = QtWidgets.QWidget() + self.stackPage1.setObjectName("stackPage1") + self.gridLayout_3 = QtWidgets.QGridLayout(self.stackPage1) self.gridLayout_3.setContentsMargins(3, 3, 3, 3) self.gridLayout_3.setSpacing(3) self.gridLayout_3.setObjectName("gridLayout_3") - self.logy_box = QtWidgets.QCheckBox(self.page) + self.logy_box = QtWidgets.QCheckBox(self.stackPage1) self.logy_box.setLayoutDirection(QtCore.Qt.RightToLeft) self.logy_box.setObjectName("logy_box") self.gridLayout_3.addWidget(self.logy_box, 2, 1, 1, 1) - self.logx_box = QtWidgets.QCheckBox(self.page) + self.logx_box = QtWidgets.QCheckBox(self.stackPage1) self.logx_box.setLayoutDirection(QtCore.Qt.RightToLeft) self.logx_box.setObjectName("logx_box") self.gridLayout_3.addWidget(self.logx_box, 2, 0, 1, 1) - self.graphicsView = GraphicsLayoutWidget(self.page) + self.graphicsView = GraphicsLayoutWidget(self.stackPage1) self.graphicsView.setObjectName("graphicsView") self.gridLayout_3.addWidget(self.graphicsView, 0, 0, 1, 2) - self.stack.addItem(self.page, "") - self.page_2 = QtWidgets.QWidget() - self.page_2.setGeometry(QtCore.QRect(0, 0, 399, 346)) - self.page_2.setObjectName("page_2") - self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.page_2) + self.stack.addTab(self.stackPage1, "") + self.stackPage2 = QtWidgets.QWidget() + self.stackPage2.setObjectName("stackPage2") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.stackPage2) self.verticalLayout_2.setContentsMargins(3, 3, 3, 3) self.verticalLayout_2.setSpacing(3) self.verticalLayout_2.setObjectName("verticalLayout_2") - self.stats_tableWidget = QtWidgets.QTableWidget(self.page_2) + self.stats_tableWidget = QtWidgets.QTableWidget(self.stackPage2) self.stats_tableWidget.setFrameShape(QtWidgets.QFrame.Box) self.stats_tableWidget.setGridStyle(QtCore.Qt.NoPen) self.stats_tableWidget.setColumnCount(1) @@ -133,15 +168,14 @@ class Ui_Dialog(object): self.stats_tableWidget.horizontalHeader().setVisible(False) self.stats_tableWidget.horizontalHeader().setSortIndicatorShown(True) self.verticalLayout_2.addWidget(self.stats_tableWidget) - self.stack.addItem(self.page_2, "") - self.page_3 = QtWidgets.QWidget() - self.page_3.setGeometry(QtCore.QRect(0, 0, 399, 346)) - self.page_3.setObjectName("page_3") - self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.page_3) + self.stack.addTab(self.stackPage2, "") + self.stackPage3 = QtWidgets.QWidget() + self.stackPage3.setObjectName("stackPage3") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.stackPage3) self.verticalLayout_3.setContentsMargins(3, 3, 3, 3) self.verticalLayout_3.setSpacing(3) self.verticalLayout_3.setObjectName("verticalLayout_3") - self.corr_tableWidget = QtWidgets.QTableWidget(self.page_3) + self.corr_tableWidget = QtWidgets.QTableWidget(self.stackPage3) self.corr_tableWidget.setFrameShape(QtWidgets.QFrame.Box) self.corr_tableWidget.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) self.corr_tableWidget.setGridStyle(QtCore.Qt.NoPen) @@ -159,28 +193,34 @@ class Ui_Dialog(object): self.corr_tableWidget.horizontalHeader().setStretchLastSection(True) self.corr_tableWidget.verticalHeader().setVisible(False) self.verticalLayout_3.addWidget(self.corr_tableWidget) - self.stack.addItem(self.page_3, "") + self.stack.addTab(self.stackPage3, "") self.gridLayout.addWidget(self.stack, 0, 1, 3, 1) self.retranslateUi(Dialog) self.stack.setCurrentIndex(0) - self.stack.layout().setSpacing(0) QtCore.QMetaObject.connectSlotsByName(Dialog) def retranslateUi(self, Dialog): _translate = QtCore.QCoreApplication.translate Dialog.setWindowTitle(_translate("Dialog", "Fit results")) self.groupBox.setTitle(_translate("Dialog", "Output")) - self.graph_checkBox.setText(_translate("Dialog", "New graph")) + self.extrapolate_box.setToolTip(_translate("Dialog", "Extrapolates only main function")) + self.extrapolate_box.setText(_translate("Dialog", "Extrapolate curves")) + self.parameter_checkbox.setText(_translate("Dialog", "Plot parameter")) + self.graph_checkBox.setText(_translate("Dialog", "New graph for parameter")) + self.minx_line.setToolTip(_translate("Dialog", "Leave empty to start at lowest point")) + self.minx_line.setPlaceholderText(_translate("Dialog", "min x")) + self.maxx_line.setToolTip(_translate("Dialog", "Leave empty to start at highest point")) + self.maxx_line.setPlaceholderText(_translate("Dialog", "max x")) + self.numx_line.setPlaceholderText(_translate("Dialog", "# pts")) self.curve_checkbox.setText(_translate("Dialog", "Plot fit curve")) self.partial_checkBox.setText(_translate("Dialog", "Plot partial functions")) - self.parameter_checkbox.setText(_translate("Dialog", "Plot parameter")) self.reject_fit_checkBox.setText(_translate("Dialog", "Reject this fit")) self.del_prev_checkBox.setText(_translate("Dialog", "Delete previous fits")) self.logy_box.setText(_translate("Dialog", "logarithmic y axis")) self.logx_box.setText(_translate("Dialog", "logarithmic x axis")) - self.stack.setItemText(self.stack.indexOf(self.page), _translate("Dialog", "Plot")) - self.stack.setItemText(self.stack.indexOf(self.page_2), _translate("Dialog", "Statistics")) + self.stack.setTabText(self.stack.indexOf(self.stackPage1), _translate("Dialog", "Plot")) + self.stack.setTabText(self.stack.indexOf(self.stackPage2), _translate("Dialog", "Statistics")) item = self.corr_tableWidget.horizontalHeaderItem(0) item.setText(_translate("Dialog", "Parameter 1")) item = self.corr_tableWidget.horizontalHeaderItem(1) @@ -189,6 +229,6 @@ class Ui_Dialog(object): item.setText(_translate("Dialog", "Corr.")) item = self.corr_tableWidget.horizontalHeaderItem(3) item.setText(_translate("Dialog", "Partial Corr.")) - self.stack.setItemText(self.stack.indexOf(self.page_3), _translate("Dialog", "Correlations")) + self.stack.setTabText(self.stack.indexOf(self.stackPage3), _translate("Dialog", "Correlations")) from ..lib.forms import ElideComboBox from pyqtgraph import GraphicsLayoutWidget diff --git a/src/gui_qt/data/container.py b/src/gui_qt/data/container.py index c9acdce..b4c2d69 100644 --- a/src/gui_qt/data/container.py +++ b/src/gui_qt/data/container.py @@ -553,7 +553,9 @@ class FitContainer(ExperimentContainer): setattr(self, n, getattr(data, n)) def _init_plot(self, **kwargs): - color = kwargs.get('color', (0, 0, 0)) + color = kwargs.get('color') + if color is None: + color = kwargs.get('linecolor', (0, 0, 0)) if isinstance(color, BaseColor): color = color.rgb() @@ -605,7 +607,7 @@ class SignalContainer(ExperimentContainer): linecolor = kwargs.get('linecolor', color) if symcolor is None and linecolor is None: - color = next(self.colors) + color = next(self.colors) if color is None else color symcolor = color linecolor = color elif symcolor is None: diff --git a/src/gui_qt/data/datawidget/datawidget.py b/src/gui_qt/data/datawidget/datawidget.py index 4cbe668..c40089a 100644 --- a/src/gui_qt/data/datawidget/datawidget.py +++ b/src/gui_qt/data/datawidget/datawidget.py @@ -17,6 +17,7 @@ class DataTree(QtWidgets.QTreeWidget): moveItem = QtCore.pyqtSignal(list, str, str, int) # items, from, to, new row copyItem = QtCore.pyqtSignal(list, str) saveFits = QtCore.pyqtSignal(list) + extendFits = QtCore.pyqtSignal(list) def __init__(self, parent=None): super().__init__(parent=parent) @@ -387,6 +388,10 @@ class DataTree(QtWidgets.QTreeWidget): for c in available_cycles.keys(): col_menu.addAction(c) + action = menu.exec(evt.globalPos()) + if action is None: + return + graphs = [] items = [] for i in self.selectedIndexes(): @@ -395,7 +400,6 @@ class DataTree(QtWidgets.QTreeWidget): items.append(self.itemFromIndex(i)) graphs.append(self.itemFromIndex(i).data(0, QtCore.Qt.UserRole)) - action = menu.exec(evt.globalPos()) if action == del_action: for gid in graphs: self.management.delete_graph(gid) @@ -414,8 +418,7 @@ class DataTree(QtWidgets.QTreeWidget): del_action = menu.addAction('Exterminate sets') cp_action = menu.addAction('Replicate sets') cat_action = menu.addAction('Join us!') - plt_action = None - save_action = None + plt_action = save_action = extend_action = None menu.addSeparator() col_menu = menu.addMenu('Color cycle') for c in available_cycles.keys(): @@ -446,6 +449,7 @@ class DataTree(QtWidgets.QTreeWidget): menu.addSeparator() plt_action = menu.addAction('Plot fit parameter') save_action = menu.addAction('Save fit parameter') + extend_action = menu.addAction('Extrapolate fit') action = menu.exec(evt.globalPos()) @@ -469,6 +473,9 @@ class DataTree(QtWidgets.QTreeWidget): elif action == save_action: self.saveFits.emit(s) + elif action == extend_action: + self.extendFits.emit(s) + elif action.parent() == col_menu: self.management.set_cycle(s, action.text()) diff --git a/src/gui_qt/fit/result.py b/src/gui_qt/fit/result.py index 9fd24a4..43e63e0 100644 --- a/src/gui_qt/fit/result.py +++ b/src/gui_qt/fit/result.py @@ -11,7 +11,7 @@ from ..lib.pg_objects import PlotItem class QFitResult(QtWidgets.QDialog, Ui_Dialog): - closed = QtCore.pyqtSignal(dict, list, str, bool, dict) + closed = QtCore.pyqtSignal(dict, list, str, bool, bool, list) redoFit = QtCore.pyqtSignal(dict) def __init__(self, results: list, management, parent=None): @@ -20,10 +20,17 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): self._management = management + self.maxx_line.setValidator(QtGui.QDoubleValidator()) + self.minx_line.setValidator(QtGui.QDoubleValidator()) + self.numx_line.setValidator(QtGui.QIntValidator()) + self.extrapolate_box.stateChanged.connect(lambda x: self.maxx_line.setEnabled(x)) + self.extrapolate_box.stateChanged.connect(lambda x: self.minx_line.setEnabled(x)) + self.extrapolate_box.stateChanged.connect(lambda x: self.numx_line.setEnabled(x)) + self._prevs = {} self._models = {} - for (res, parts) in results: + for res in results: idx = res.idx data_k = management.data[idx] @@ -36,8 +43,7 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): for fit in data_k.get_fits(): self._prevs[idx].append((fit.name, fit.statistics, fit.nobs-fit.nvar)) - self._results = {res.idx: res for (res, _) in results} - self._parts = {res.idx: parts for (res, parts) in results} + self._results = {res.idx: res for res in results} self._opts = [(False, False) for _ in range(len(self._results))] self.residplot = self.graphicsView.addPlot(row=0, col=0) @@ -273,12 +279,75 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): plot_fits = self.curve_checkbox.isChecked() - if self.partial_checkBox.checkState() == QtCore.Qt.Checked: - self.closed.emit(self._results, self._opts, graph, plot_fits, self._parts) - else: - self.closed.emit(self._results, self._opts, graph, plot_fits, {}) + parts = self.partial_checkBox.checkState() == QtCore.Qt.Checked + + extrapolate = [None, None, None] + if self.extrapolate_box.isChecked(): + try: + extrapolate[0] = float(self.minx_line.text()) + except TypeError: + pass + try: + extrapolate[1] = float(self.maxx_line.text()) + except TypeError: + pass + try: + extrapolate[2] = int(self.numx_line.text()) + except TypeError: + pass + + + self.closed.emit(self._results, self._opts, graph, plot_fits, parts, extrapolate) self.accept() else: self.reject() + + +class FitExtension(QtWidgets.QDialog): + def __init__(self, parent=None): + super().__init__(parent=parent) + gridLayout = QtWidgets.QGridLayout(self) + + self.label = QtWidgets.QLabel('Minimum value') + gridLayout.addWidget(self.label, 0, 0, 1, 1) + + self.min_line = QtWidgets.QLineEdit() + self.min_line.setValidator(QtGui.QDoubleValidator()) + gridLayout.addWidget(self.min_line, 0, 1, 1, 1) + + self.label_2 = QtWidgets.QLabel('Maximum value') + gridLayout.addWidget(self.label_2, 1, 0, 1, 1) + + self.max_line = QtWidgets.QLineEdit() + self.max_line.setValidator(QtGui.QDoubleValidator()) + gridLayout.addWidget(self.max_line, 1, 1, 1, 1) + + self.label_3 = QtWidgets.QLabel('Number of pts.') + gridLayout.addWidget(self.label_3, 2, 0, 1, 1) + + self.num_pts = QtWidgets.QLineEdit() + self.num_pts.setValidator(QtGui.QIntValidator()) + gridLayout.addWidget(self.num_pts, 2, 1, 1, 1) + + self.buttonBox = QtWidgets.QDialogButtonBox() + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + gridLayout.addWidget(self.buttonBox, 3, 0, 1, 2) + + self.setLayout(gridLayout) + + self.buttonBox.accepted.connect(self.accept) + self.buttonBox.rejected.connect(self.reject) + + @property + def values(self): + try: + xmin = float(self.min_line.text()) + xmax = float(self.max_line.text()) + nums = int(self.num_pts.text()) + except TypeError: + return None + + return xmin, xmax, nums \ No newline at end of file diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 7212234..444fd1f 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -11,10 +11,10 @@ from pyqtgraph import ViewBox from nmreval.configs import * from .management import UpperManagement -from ..Qt import QtCore, QtGui, QtPrintSupport, QtWidgets +from ..Qt import QtGui, QtPrintSupport from ..data.shift_graphs import QShift from ..data.signaledit import QApodDialog, QBaselineDialog, QPhasedialog -from ..fit.result import QFitResult +from ..fit.result import FitExtension, QFitResult from ..graphs.graphwindow import QGraphWindow from ..graphs.movedialog import QMover from ..io.fcbatchreader import QFCReader @@ -164,6 +164,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.datawidget.startShowProperty.connect(self.management.get_properties) self.datawidget.propertyChanged.connect(self.management.update_property) self.datawidget.tree.saveFits.connect(self.save_fit_parameter) + self.datawidget.tree.extendFits.connect(self.extend_fit) self.management.newData.connect(self.show_new_data) self.management.newGraph.connect(self.new_graph) @@ -907,10 +908,10 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): res_dialog.redoFit.connect(self.management.redo_fits) res_dialog.show() - @QtCore.pyqtSlot(dict, list, str, bool, dict) - def accepts_fit(self, res: dict, opts: list, param_graph: str, show_fit: bool, parts: dict) -> None: + @QtCore.pyqtSlot(dict, list, str, bool, bool, list) + def accepts_fit(self, res: dict, opts: list, param_graph: str, show_fit: bool, parts: bool, extrapolate: list) -> None: self.fit_dialog.set_parameter(res) - self.management.make_fits(res, opts, param_graph, show_fit, parts) + self.management.make_fits(res, opts, param_graph, show_fit, parts, extrapolate) @QtCore.pyqtSlot(name='on_actionFunction_editor_triggered') def edit_models(self): @@ -922,6 +923,16 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.editor.setWindowModality(QtCore.Qt.ApplicationModal) self.editor.show() + @QtCore.pyqtSlot(list) + def extend_fit(self, sets: list): + w = FitExtension(self) + res = w.exec() + print(res) + if res: + p = w.values + x = linspace(p[0], p[1], num=p[2]) + self.management.extend_fits(sets, x) + @QtCore.pyqtSlot(name='on_action_create_fit_function_triggered') def open_fitmodel_wizard(self): from ..fit.function_creation_dialog import QUserFitCreator @@ -931,7 +942,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): helper.show() - @QtCore.pyqtSlot(name='on_actionShift_triggered') def shift_dialog(self): s = QShift(self) diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index 2a2549f..51a2b10 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -4,6 +4,8 @@ import pathlib import re import uuid +import numpy as np + from nmreval.fit import data as fit_d from nmreval.fit.model import Model from nmreval.fit.result import FitResult @@ -482,7 +484,7 @@ class UpperManagement(QtCore.QObject): parameter[set_id] = (new_values, set_parameter[1]) self.start_fit(*self.__fit_options) - def make_fits(self, res: dict, opts: list, param_graph: str, show_fit: bool, parts: dict) -> None: + def make_fits(self, res: dict, opts: list, param_graph: str, show_fit: bool, parts: bool, extrapolate: list) -> None: """ Args: @@ -491,6 +493,7 @@ class UpperManagement(QtCore.QObject): param_graph: None if no parameter to plot, '' for new graph, or id of existig graph show_fit: plot fit curve? parts: key is that of original data, value is list of subplots + extrapolate: """ f_id_list = [] @@ -503,6 +506,26 @@ class UpperManagement(QtCore.QObject): if reject: continue + if not all(e is None for e in extrapolate): + spacefunc = np.geomspace if fit.islog else np.linspace + + xmin = fit.x.min() + xmax = fit.x.max() + + len_data = len(fit.x_data) + num_pts = 20*len_data-9 if len_data < 51 else 3*len_data + + if extrapolate[0] is not None: + xmin = extrapolate[0] + if extrapolate[1] is not None: + xmax = extrapolate[1] + if extrapolate[2] is not None: + num_pts = extrapolate[2] + + _x = spacefunc(xmin, xmax, num=num_pts) + + fit = fit.with_new_x(_x) + data_k = self.data[k] if delete_prev: tobedeleted.extend([f.id for f in data_k.get_fits()]) @@ -527,18 +550,16 @@ class UpperManagement(QtCore.QObject): f_id_list.append(f_id) data_k.set_fits(f_id) + if parts: + color_scheme = available_cycles['colorblind'] + for subfunc, col in zip(fit.sub(fit.x), cycle(color_scheme)): + subfunc.value = data_k.value + subfunc.group = data_k.group + subfunc.name += data_name + sub_f_id = self.add(subfunc, color=col, linestyle=LineStyle.Dashed, symbol=SymbolStyle.No) + + f_id_list.append(sub_f_id) gid = data_k.graph - - if k in parts and show_fit: - color_scheme = available_cycles['colorblind'] - for subfunc, col in zip(parts[k], cycle(color_scheme)): - subfunc.value = data_k.value - subfunc.group = data_k.group - subfunc.name += data_name - sub_f_id = self.add(subfunc, color=col, linestyle=LineStyle.Dashed, symbol=SymbolStyle.No) - - f_id_list.append(sub_f_id) - self.delete_sets(tobedeleted) if accepted and (param_graph != '-1'): @@ -546,6 +567,27 @@ class UpperManagement(QtCore.QObject): self.newData.emit(f_id_list, gid) + def extend_fits(self, set_id: list, x_range: np.ndarray): + graphs = {} + for sid in set_id: + data = self[sid] + fit = data.copy(full=True, keep_color=True) + fit.data = fit.data.with_new_x(x_range) + + graph_id = data.graph + if graph_id not in graphs: + graphs[graph_id] = [] + graphs[graph_id].append(self.add(fit)) + + color_scheme = available_cycles['colorblind'] + for subfunc, col in zip(fit.data.sub(fit.x), cycle(color_scheme)): + subfunc.value = fit.value + subfunc.group = fit.group + graphs[graph_id].append(self.add(subfunc, color=col, linestyle=LineStyle.Dashed, symbol=SymbolStyle.No)) + + for k, v in graphs.items(): + self.newData.emit(v, k) + def make_fit_parameter(self, fit_sets: list[str | FitResult], graph_id: str = None): fit_dict = self._collect_fit_parameter(fit_sets) diff --git a/src/nmreval/fit/model.py b/src/nmreval/fit/model.py index 631c715..c0121da 100644 --- a/src/nmreval/fit/model.py +++ b/src/nmreval/fit/model.py @@ -126,12 +126,12 @@ class Model(object): kwargs = self.fun_kwargs if not self.is_multi: - return [self.func(p, x, **kwargs)] + return [] else: return list(self._int_iter(x, *p, *self.fun_args, **kwargs)) def sub_name(self): if not self.is_multi: - return [self.name] + return [] else: return list(self._iter_name()) diff --git a/src/nmreval/fit/result.py b/src/nmreval/fit/result.py index 8b705ef..dcb0b6d 100644 --- a/src/nmreval/fit/result.py +++ b/src/nmreval/fit/result.py @@ -9,6 +9,7 @@ import numpy as np from scipy.stats import f as fdist from scipy.interpolate import interp1d +from ._meta import MultiModel from .parameter import Parameter from ..data.points import Points from ..data.signals import Signal @@ -17,7 +18,7 @@ from ..utils.text import convert class FitResultCreator: @staticmethod - def make_from_session(x_orig: np.ndarray, y_orig: np.ndarray, idx: int, kwargs: dict[Any]) -> (dict, list): + def make_from_session(x_orig: np.ndarray, y_orig: np.ndarray, idx: int, kwargs: dict[Any]) -> FitResult: params = OrderedDict() for key, pbest, err in zip(kwargs['pnames'], kwargs['parameter'], kwargs['error']): @@ -37,10 +38,10 @@ class FitResultCreator: stats = FitResultCreator.calc_statistics(resid, _y) return FitResult(kwargs['x'], kwargs['y'], x_orig, y_orig, params, dict(kwargs['choice']), resid, 0, 0, - kwargs['name'], stats, idx), [] + kwargs['name'], stats, idx) @staticmethod - def make_with_model(model, x_orig, y_orig, p, fun_kwargs, idx, nobs, nvar, corr, pcorr) -> (dict, list): + def make_with_model(model, x_orig, y_orig, p, fun_kwargs, idx, nobs, nvar, corr, pcorr) -> FitResult: if np.all(x_orig > 0) and (np.max(x_orig) > 100 * np.min(x_orig)): islog = True else: @@ -48,7 +49,7 @@ class FitResultCreator: if len(x_orig) < 51: if islog: - _x = np.logspace(np.log10(np.min(x_orig)), np.log10(np.max(x_orig)), num=10*x_orig.size-9) + _x = np.geomspace(np.min(x_orig), np.max(x_orig), num=10*x_orig.size-9) else: _x = np.linspace(np.min(x_orig), np.max(x_orig), num=10*x_orig.size-9) else: @@ -62,15 +63,6 @@ class FitResultCreator: parameters = OrderedDict([(k, v) for k, v in zip(pnames, p)]) p_final = [p.value for p in parameters.values()] - part_functions = [] - - if model.is_multi: - for sub_name, sub_y in zip(model.sub_name(), model.sub(p_final, _x, **fun_kwargs)): - if np.iscomplexobj(sub_y): - part_functions.append(Signal(_x, sub_y, name=sub_name)) - else: - part_functions.append(Points(_x, sub_y, name=sub_name)) - _y = model.func(p_final, _x, **fun_kwargs) resid = model.func(p_final, x_orig, **fun_kwargs) - y_orig @@ -97,13 +89,10 @@ class FitResultCreator: correlation = corr partial_correlation = pcorr - return ( - FitResult(_x, _y, x_orig, y_orig, parameters, fun_kwargs, resid, - nobs, nvar, model.name, stats, - idx=idx, corr=correlation, pcorr=partial_correlation, - islog=islog), - part_functions, - ) + return FitResult(_x, _y, x_orig, y_orig, parameters, fun_kwargs, resid, + nobs, nvar, model.name, stats, + idx=idx, corr=correlation, pcorr=partial_correlation, + islog=islog, func=model) @staticmethod def calc_statistics(y, residual, nobs=None, nvar=None): @@ -141,8 +130,11 @@ class FitResultCreator: class FitResult(Points): - def __init__(self, x, y, x_data, y_data, params, fun_kwargs, resid, nobs, nvar, name, stats, - idx=None, corr=None, pcorr=None, islog=False, + def __init__(self, x: np.ndarray, y: np.ndarray, + x_data: np.ndarray, y_data: np.ndarray, + params: dict, fun_kwargs: dict, + resid: np.ndarray, nobs: int, nvar: int, name: str, stats: dict, + idx=None, corr=None, pcorr=None, islog=False, func=None, **kwargs): self.parameter, name = self._prepare_names(params, name) @@ -162,6 +154,7 @@ class FitResult(Points): self.x_data = x_data self.y_data = y_data self._model_name = name + self._func = func @staticmethod def _prepare_names(parameter: dict, modelname: str): @@ -200,6 +193,13 @@ class FitResult(Points): except AttributeError: return 'FitObject' + @property + def func(self): + if isinstance(self._func, MultiModel): + return self._func.func + else: + return self._func + @property def p_final(self): return [pp.value for pp in self.parameter.values()] @@ -215,6 +215,7 @@ class FitResult(Points): print(' #var :', self.nvar) print('\nParameter') print(self._parameter_string()) + if statistics: print('Statistics') for k, v in self.statistics.items(): @@ -342,3 +343,24 @@ class FitResult(Points): data = FitResult(**state) return data + + def with_new_x(self, x_values): + if self.func is None: + raise ValueError('no fit function available to calcualate new y values') + + new_fit = self.copy() + y_values = self.func.func(self.p_final, x_values, **self.fun_kwargs) + new_fit.set_data(x_values, y_values) + + return new_fit + + + def sub(self, x_values): + part_functions = [] + for sub_name, sub_y in zip(self.func.sub_name(), self.func.sub(self.p_final, x_values, **self.fun_kwargs)): + if np.iscomplexobj(sub_y): + part_functions.append(Signal(x_values, sub_y, name=sub_name)) + else: + part_functions.append(Points(x_values, sub_y, name=sub_name)) + + return part_functions \ No newline at end of file diff --git a/src/resources/_ui/fitresult.ui b/src/resources/_ui/fitresult.ui index 33e21c0..c6dd2c0 100644 --- a/src/resources/_ui/fitresult.ui +++ b/src/resources/_ui/fitresult.ui @@ -6,8 +6,8 @@ 0 0 - 817 - 584 + 864 + 649 @@ -98,7 +98,37 @@ 3 - + + + + Extrapolates only main function + + + Extrapolate curves + + + + + + + Plot parameter + + + + + + + false + + + + 0 + 0 + + + + + @@ -107,46 +137,88 @@ - New graph + New graph for parameter true + + + + false + + + + 0 + 0 + + + + Leave empty to start at lowest point + + + min x + + + + + + + Qt::Vertical + + + - + false - - - - - - Plot fit curve + + + 0 + 0 + - - true + + Leave empty to start at highest point + + + max x - - - - Plot partial functions + + + + false + + + # pts - - - - Plot parameter - - - true - - + + + + + + Plot fit curve + + + true + + + + + + + Plot partial functions + + + + @@ -180,7 +252,7 @@ - + 0 @@ -190,19 +262,8 @@ 0 - - 0 - - - - - 0 - 0 - 399 - 346 - - - + + Plot @@ -246,16 +307,8 @@ - - - - 0 - 0 - 399 - 346 - - - + + Statistics @@ -296,16 +349,8 @@ - - - - 0 - 0 - 399 - 346 - - - + + Correlations