From d83e1125153fd432c4acd5d671e04b329a0747ff Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 11 Jan 2024 12:39:03 +0000 Subject: [PATCH 01/30] bugfixes-022024 (#211) Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/211 --- src/gui_qt/_py/fcreader.py | 15 +++++++------ src/gui_qt/fit/result.py | 4 ---- src/gui_qt/io/fcbatchreader.py | 39 +++++++++++++++++++++------------- src/resources/_ui/fcreader.ui | 11 ++++++++-- 4 files changed, 42 insertions(+), 27 deletions(-) diff --git a/src/gui_qt/_py/fcreader.py b/src/gui_qt/_py/fcreader.py index 766c641..041a46c 100644 --- a/src/gui_qt/_py/fcreader.py +++ b/src/gui_qt/_py/fcreader.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'resources/_ui/fcreader.ui' +# Form implementation generated from reading ui file 'src/resources/_ui/fcreader.ui' # -# Created by: PyQt5 UI code generator 5.12.3 +# Created by: PyQt5 UI code generator 5.15.10 # -# WARNING! All changes made in this file will be lost! +# 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. from PyQt5 import QtCore, QtGui, QtWidgets @@ -47,6 +48,7 @@ class Ui_FCEval_dialog(object): self.verticalLayout.addWidget(self.input_box) self.region_box = QtWidgets.QGroupBox(FCEval_dialog) self.region_box.setCheckable(True) + self.region_box.setChecked(False) self.region_box.setObjectName("region_box") self.horizontalLayout = QtWidgets.QHBoxLayout(self.region_box) self.horizontalLayout.setContentsMargins(3, 3, 3, 3) @@ -139,6 +141,7 @@ class Ui_FCEval_dialog(object): self.line.setObjectName("line") self.gridLayout.addWidget(self.line, 2, 0, 1, 2) self.graph_comboBox = QtWidgets.QComboBox(self.out_box) + self.graph_comboBox.setEnabled(False) self.graph_comboBox.setObjectName("graph_comboBox") self.gridLayout.addWidget(self.graph_comboBox, 3, 1, 1, 1) self.graph_checkbox = QtWidgets.QCheckBox(self.out_box) @@ -167,8 +170,8 @@ class Ui_FCEval_dialog(object): self.label_6.setBuddy(self.m0_cb) self.retranslateUi(FCEval_dialog) - self.buttonBox.accepted.connect(FCEval_dialog.accept) - self.buttonBox.rejected.connect(FCEval_dialog.reject) + self.buttonBox.accepted.connect(FCEval_dialog.accept) # type: ignore + self.buttonBox.rejected.connect(FCEval_dialog.reject) # type: ignore QtCore.QMetaObject.connectSlotsByName(FCEval_dialog) def retranslateUi(self, FCEval_dialog): @@ -178,7 +181,7 @@ class Ui_FCEval_dialog(object): self.file_pushbutton.setText(_translate("FCEval_dialog", "Add HDF files...")) self.dir_pushbutton.setText(_translate("FCEval_dialog", "Add directory...")) self.overwrite_cb.setText(_translate("FCEval_dialog", "Overwrite prev. data")) - self.region_box.setTitle(_translate("FCEval_dialog", "Evaluate region (empty values default to start/end)")) + self.region_box.setTitle(_translate("FCEval_dialog", "Evaluate region (empty values default to values of the script)")) self.start_lineedit.setPlaceholderText(_translate("FCEval_dialog", "start pos in µs")) self.stop_lineedit.setPlaceholderText(_translate("FCEval_dialog", "end pos in µs")) self.fit_box.setTitle(_translate("FCEval_dialog", "Fit equation")) diff --git a/src/gui_qt/fit/result.py b/src/gui_qt/fit/result.py index 01f44ee..eeaa56b 100644 --- a/src/gui_qt/fit/result.py +++ b/src/gui_qt/fit/result.py @@ -229,20 +229,16 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): self.fit_plot.enableAutoRange() def _plot_residuals(self, idx: str = None): - print(idx) if idx is None or isinstance(idx, QtWidgets.QAbstractButton): idx = self.sets_comboBox.currentData(QtCore.Qt.ItemDataRole.UserRole) res = self._results[idx] if res.iscomplex: if self.rel_dev_button.isChecked(): - print('rel') self.resid_graph.setData(x=res.x_data, y=res.residual.real/np_abs(res.y_data.real)) - print(res.y_data.imag) if all(np_isfinite(res.y_data.imag)): self.resid_graph_imag.setData(x=res.x_data, y=res.residual.imag/np_abs(res.y_data.imag)) else: - print('abs') self.resid_graph.setData(x=res.x_data, y=res.residual.real) self.resid_graph_imag.setData(x=res.x_data, y=res.residual.imag) diff --git a/src/gui_qt/io/fcbatchreader.py b/src/gui_qt/io/fcbatchreader.py index 4f678b5..c4bb80b 100644 --- a/src/gui_qt/io/fcbatchreader.py +++ b/src/gui_qt/io/fcbatchreader.py @@ -22,14 +22,16 @@ class QFCReader(QtWidgets.QDialog, Ui_FCEval_dialog): self.start_lineedit.setValidator(QtGui.QDoubleValidator()) self.stop_lineedit.setValidator(QtGui.QDoubleValidator()) - self.graph_checkbox.stateChanged.connect(lambda x: self.graph_comboBox.setEnabled(not bool(x))) + self.graph_checkbox.stateChanged.connect( + lambda x: self.graph_comboBox.setEnabled(x == QtCore.Qt.CheckState.Unchecked) + ) self.listWidget.installEventFilter(self) def eventFilter(self, src: QtCore.QObject, evt: QtCore.QEvent) -> bool: # intercept key press in listwidget to allow deletion with Del - if evt.type() == QtCore.QEvent.KeyPress: - if evt.key() == QtCore.Qt.Key_Delete: + if evt.type() == QtCore.QEvent.Type.KeyPress: + if evt.key() == QtCore.Qt.Key.Key_Delete: self.listWidget.takeItem(self.listWidget.currentRow()) return True @@ -41,21 +43,25 @@ class QFCReader(QtWidgets.QDialog, Ui_FCEval_dialog): @QtCore.pyqtSlot(int, name='on_region_checkBox_stateChanged') def use_region(self, state: int): - self.start_lineedit.setEnabled(state == QtCore.Qt.Checked) - self.stop_lineedit.setEnabled(state == QtCore.Qt.Checked) + self.start_lineedit.setEnabled(state == QtCore.Qt.CheckState.Checked) + self.stop_lineedit.setEnabled(state == QtCore.Qt.CheckState.Checked) @QtCore.pyqtSlot(name='on_file_pushbutton_clicked') @QtCore.pyqtSlot(name='on_dir_pushbutton_clicked') def get_input(self): if self.sender() == self.file_pushbutton: - infiles, _ = QtWidgets.QFileDialog.getOpenFileNames(caption='Select HDF files', - directory=str(self.path), - filter='HDF files (*.h5)') + infiles, _ = QtWidgets.QFileDialog.getOpenFileNames( + caption='Select HDF files', + directory=str(self.path), + filter='HDF files (*.h5)', + ) else: - infiles = QtWidgets.QFileDialog.getExistingDirectory(caption='Select input directory', - directory=str(self.path), - options=QtWidgets.QFileDialog.ShowDirsOnly) + infiles = QtWidgets.QFileDialog.getExistingDirectory( + caption='Select input directory', + directory=str(self.path), + options=QtWidgets.QFileDialog.ShowDirsOnly, + ) infiles = [infiles] if infiles else infiles if infiles: @@ -65,9 +71,12 @@ class QFCReader(QtWidgets.QDialog, Ui_FCEval_dialog): @QtCore.pyqtSlot(name='on_savebutton_clicked') def save_path(self): - outfile = QtWidgets.QFileDialog.getExistingDirectory(self, caption='Select directory', - directory=self.label.text(), - options=QtWidgets.QFileDialog.ShowDirsOnly) + outfile = QtWidgets.QFileDialog.getExistingDirectory( + self, + caption='Select directory', + directory=self.label.text(), + options=QtWidgets.QFileDialog.ShowDirsOnly, + ) if outfile: self.label.setText(outfile) @@ -110,6 +119,6 @@ class QFCReader(QtWidgets.QDialog, Ui_FCEval_dialog): grp = '' if not self.graph_checkbox.isChecked(): - grp = self.graph_comboBox.currentData(QtCore.Qt.UserRole) + grp = self.graph_comboBox.currentData(QtCore.Qt.ItemDataRole.UserRole) self.data_read.emit(ret_vals, grp) diff --git a/src/resources/_ui/fcreader.ui b/src/resources/_ui/fcreader.ui index 7c818de..ff4695c 100644 --- a/src/resources/_ui/fcreader.ui +++ b/src/resources/_ui/fcreader.ui @@ -96,11 +96,14 @@ - Evaluate region (empty values default to start/end) + Evaluate region (empty values default to values of the script) true + + false + 3 @@ -311,7 +314,11 @@ - + + + false + + From 38a44047dec683e6d74188fad72b307fd65e5ad4 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 15 Jan 2024 18:41:37 +0000 Subject: [PATCH 02/30] set new y_err with value zero (#215) closes #215 --- .gitea/workflows/build-appimage.yaml | 4 +-- src/nmreval/math/smooth.py | 51 ++++++++++++++++++++-------- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/.gitea/workflows/build-appimage.yaml b/.gitea/workflows/build-appimage.yaml index 2c43c0b..1b69b69 100644 --- a/.gitea/workflows/build-appimage.yaml +++ b/.gitea/workflows/build-appimage.yaml @@ -29,10 +29,10 @@ jobs: env: GPG_KEYGRIP: ${{ vars.GPG_KEYGRIP }} GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - GO_PIPELINE_LABEL: ${{ env.YEAR }}.${{ gitea.run_number }}_${{ env.SHA_SHORT }}_bookworm + GO_PIPELINE_LABEL: ${{ env.YEAR }}.${{ gitea.run_number }}_${{ env.SHA_SHORT }} - name: Upload AppImage run: ./tools/upload_gitea.sh env: - GO_PIPELINE_LABEL: ${{ env.YEAR }}.${{ gitea.run_number }}_${{ env.SHA_SHORT }}_bookworm + GO_PIPELINE_LABEL: ${{ env.YEAR }}.${{ gitea.run_number }}_${{ env.SHA_SHORT }} UPLOAD_TOKEN: ${{ secrets.UPLOAD_TOKEN }} UPLOAD_USER: ${{ vars.UPLOAD_USER }} diff --git a/src/nmreval/math/smooth.py b/src/nmreval/math/smooth.py index 5207720..fc5997d 100644 --- a/src/nmreval/math/smooth.py +++ b/src/nmreval/math/smooth.py @@ -3,14 +3,21 @@ import numpy.polynomial.polynomial as poly from scipy import signal as signal -__all__ = ['smooth', 'loess', 'savgol', - 'running_max', 'running_min', - 'running_var', 'running_std', - 'running_median', 'running_mean', - 'running_sum'] +__all__ = [ + 'smooth', + 'loess', + 'savgol', + 'running_max', + 'running_min', + 'running_var', + 'running_std', + 'running_median', + 'running_mean', + 'running_sum', +] -def loess(x, y, window_size, it=0, deg=2): +def loess(x, y, window_size: int, it: int = 0, deg: int = 2): # ULTRA LANGSAM !!! it = max(it, 0) @@ -81,19 +88,19 @@ def savgol(x, y, window_size: int, deg: int = 2, mode: str = 'mirror'): return new_y -def running_mean(x, y, window_size): +def running_mean(x, y, window_size: int): return _running_func(np.nanmean, x, y, window_size) -def running_median(x, y, window_size): +def running_median(x, y, window_size: int): return _running_func(np.nanmedian, x, y, window_size) -def running_std(x, y, window_size): +def running_std(x, y, window_size: int): return _running_func(np.nanstd, x, y, window_size) -def running_var(x, y, window_size): +def running_var(x, y, window_size: int): return _running_func(np.nanvar, x, y, window_size) @@ -132,11 +139,27 @@ def _moving_window(arr, nn): return np.lib.stride_tricks.as_strided(arr, shapes, strides) -_funcs = {'loess': loess, 'savgol': savgol, 'mean': running_mean, 'median': running_median, - 'std': running_std, 'var': running_var, 'max': running_max, 'min': running_min, 'sum': running_sum} +_funcs = { + 'loess': loess, + 'savgol': savgol, + 'mean': running_mean, + 'median': running_median, + 'std': running_std, + 'var': running_var, + 'max': running_max, + 'min': running_min, + 'sum': running_sum, +} -def smooth(data, window_size, mode='mean', logx=False, logy=False, **kwargs): +def smooth( + data: 'Data', + window_size: int, + mode: str = 'mean', + logx: bool = False, + logy: bool = False, + **kwargs +): try: func = _funcs[mode] except KeyError: @@ -162,6 +185,6 @@ def smooth(data, window_size, mode='mean', logx=False, logy=False, **kwargs): new_y = 10**new_y new_data = data.copy() - new_data.set_data(x=new_x, y=new_y, y_err=None) + new_data.set_data(x=new_x, y=new_y, y_err=np.zeros_like(new_y)) return new_data From 256bc2084608e5118989e01a2a89c17ebf60c82d Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 18 Jan 2024 17:39:12 +0100 Subject: [PATCH 03/30] add hint to sort before averaging --- src/gui_qt/_py/smoothdialog.py | 97 +++++++++-------- src/resources/_ui/smoothdialog.ui | 172 ++++++++++++++++-------------- 2 files changed, 142 insertions(+), 127 deletions(-) diff --git a/src/gui_qt/_py/smoothdialog.py b/src/gui_qt/_py/smoothdialog.py index 634fde5..7d2d758 100644 --- a/src/gui_qt/_py/smoothdialog.py +++ b/src/gui_qt/_py/smoothdialog.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'resources/_ui/smoothdialog.ui' +# Form implementation generated from reading ui file 'src/resources/_ui/smoothdialog.ui' # -# Created by: PyQt5 UI code generator 5.12.3 +# Created by: PyQt5 UI code generator 5.15.10 # -# WARNING! All changes made in this file will be lost! +# 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. from PyQt5 import QtCore, QtGui, QtWidgets @@ -17,9 +18,37 @@ class Ui_SmoothDialog(object): self.gridLayout = QtWidgets.QGridLayout(SmoothDialog) self.gridLayout.setSpacing(3) self.gridLayout.setObjectName("gridLayout") + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.gridLayout.addItem(spacerItem, 7, 0, 1, 1) self.frac_label = QtWidgets.QLabel(SmoothDialog) self.frac_label.setObjectName("frac_label") - self.gridLayout.addWidget(self.frac_label, 1, 0, 1, 1) + self.gridLayout.addWidget(self.frac_label, 2, 0, 1, 1) + self.line = QtWidgets.QFrame(SmoothDialog) + self.line.setFrameShape(QtWidgets.QFrame.HLine) + self.line.setFrameShadow(QtWidgets.QFrame.Sunken) + self.line.setObjectName("line") + self.gridLayout.addWidget(self.line, 5, 0, 1, 2) + self.widget = QtWidgets.QWidget(SmoothDialog) + self.widget.setObjectName("widget") + self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.widget) + self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_2.setSpacing(3) + self.horizontalLayout_2.setObjectName("horizontalLayout_2") + self.label = QtWidgets.QLabel(self.widget) + self.label.setObjectName("label") + self.horizontalLayout_2.addWidget(self.label) + self.polynom_spinBox = QtWidgets.QSpinBox(self.widget) + self.polynom_spinBox.setMinimum(1) + self.polynom_spinBox.setMaximum(3) + self.polynom_spinBox.setObjectName("polynom_spinBox") + self.horizontalLayout_2.addWidget(self.polynom_spinBox) + self.gridLayout.addWidget(self.widget, 3, 0, 1, 2) + self.y_checkBox = QtWidgets.QCheckBox(SmoothDialog) + self.y_checkBox.setObjectName("y_checkBox") + self.gridLayout.addWidget(self.y_checkBox, 6, 1, 1, 1) + self.x_checkBox = QtWidgets.QCheckBox(SmoothDialog) + self.x_checkBox.setObjectName("x_checkBox") + self.gridLayout.addWidget(self.x_checkBox, 6, 0, 1, 1) self.widget_2 = QtWidgets.QWidget(SmoothDialog) self.widget_2.setObjectName("widget_2") self.horizontalLayout_3 = QtWidgets.QHBoxLayout(self.widget_2) @@ -35,37 +64,17 @@ class Ui_SmoothDialog(object): self.iter_spinBox.setProperty("value", 1) self.iter_spinBox.setObjectName("iter_spinBox") self.horizontalLayout_3.addWidget(self.iter_spinBox) - self.gridLayout.addWidget(self.widget_2, 3, 0, 1, 2) - self.line = QtWidgets.QFrame(SmoothDialog) - self.line.setFrameShape(QtWidgets.QFrame.HLine) - self.line.setFrameShadow(QtWidgets.QFrame.Sunken) - self.line.setObjectName("line") - self.gridLayout.addWidget(self.line, 4, 0, 1, 2) - self.buttonBox = QtWidgets.QDialogButtonBox(SmoothDialog) - self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Apply|QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) - self.buttonBox.setObjectName("buttonBox") - self.gridLayout.addWidget(self.buttonBox, 7, 0, 1, 2) - self.widget = QtWidgets.QWidget(SmoothDialog) - self.widget.setObjectName("widget") - self.horizontalLayout_2 = QtWidgets.QHBoxLayout(self.widget) - self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) - self.horizontalLayout_2.setSpacing(3) - self.horizontalLayout_2.setObjectName("horizontalLayout_2") - self.label = QtWidgets.QLabel(self.widget) - self.label.setObjectName("label") - self.horizontalLayout_2.addWidget(self.label) - self.polynom_spinBox = QtWidgets.QSpinBox(self.widget) - self.polynom_spinBox.setMinimum(1) - self.polynom_spinBox.setMaximum(3) - self.polynom_spinBox.setObjectName("polynom_spinBox") - self.horizontalLayout_2.addWidget(self.polynom_spinBox) - self.gridLayout.addWidget(self.widget, 2, 0, 1, 2) + self.gridLayout.addWidget(self.widget_2, 4, 0, 1, 2) self.frac_spinBox = QtWidgets.QSpinBox(SmoothDialog) self.frac_spinBox.setMinimum(1) self.frac_spinBox.setMaximum(999) self.frac_spinBox.setObjectName("frac_spinBox") - self.gridLayout.addWidget(self.frac_spinBox, 1, 1, 1, 1) + self.gridLayout.addWidget(self.frac_spinBox, 2, 1, 1, 1) + self.buttonBox = QtWidgets.QDialogButtonBox(SmoothDialog) + self.buttonBox.setOrientation(QtCore.Qt.Horizontal) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Apply|QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setObjectName("buttonBox") + self.gridLayout.addWidget(self.buttonBox, 8, 0, 1, 2) self.comboBox = QtWidgets.QComboBox(SmoothDialog) self.comboBox.setObjectName("comboBox") self.comboBox.addItem("") @@ -77,22 +86,17 @@ class Ui_SmoothDialog(object): self.comboBox.addItem("") self.comboBox.addItem("") self.comboBox.addItem("") - self.gridLayout.addWidget(self.comboBox, 0, 0, 1, 2) - spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) - self.gridLayout.addItem(spacerItem, 6, 0, 1, 1) - self.y_checkBox = QtWidgets.QCheckBox(SmoothDialog) - self.y_checkBox.setObjectName("y_checkBox") - self.gridLayout.addWidget(self.y_checkBox, 5, 1, 1, 1) - self.x_checkBox = QtWidgets.QCheckBox(SmoothDialog) - self.x_checkBox.setObjectName("x_checkBox") - self.gridLayout.addWidget(self.x_checkBox, 5, 0, 1, 1) + self.gridLayout.addWidget(self.comboBox, 1, 0, 1, 2) + self.label_2 = QtWidgets.QLabel(SmoothDialog) + self.label_2.setObjectName("label_2") + self.gridLayout.addWidget(self.label_2, 0, 0, 1, 2) self.frac_label.setBuddy(self.frac_spinBox) - self.label_3.setBuddy(self.iter_spinBox) self.label.setBuddy(self.polynom_spinBox) + self.label_3.setBuddy(self.iter_spinBox) self.retranslateUi(SmoothDialog) - self.buttonBox.accepted.connect(SmoothDialog.accept) - self.buttonBox.rejected.connect(SmoothDialog.reject) + self.buttonBox.accepted.connect(SmoothDialog.accept) # type: ignore + self.buttonBox.rejected.connect(SmoothDialog.reject) # type: ignore QtCore.QMetaObject.connectSlotsByName(SmoothDialog) SmoothDialog.setTabOrder(self.comboBox, self.frac_spinBox) SmoothDialog.setTabOrder(self.frac_spinBox, self.polynom_spinBox) @@ -104,9 +108,11 @@ class Ui_SmoothDialog(object): _translate = QtCore.QCoreApplication.translate SmoothDialog.setWindowTitle(_translate("SmoothDialog", "1D smoothing filter")) self.frac_label.setText(_translate("SmoothDialog", "Window length")) - self.label_3.setText(_translate("SmoothDialog", "Iterations")) self.label.setText(_translate("SmoothDialog", "Polynomial degree")) self.polynom_spinBox.setToolTip(_translate("SmoothDialog", "Deg")) + self.y_checkBox.setText(_translate("SmoothDialog", "y log-spaced?")) + self.x_checkBox.setText(_translate("SmoothDialog", "x log-spaced?")) + self.label_3.setText(_translate("SmoothDialog", "Iterations")) self.frac_spinBox.setToolTip(_translate("SmoothDialog", "

Number of data points used as smoothing window.

")) self.comboBox.setItemText(0, _translate("SmoothDialog", "Moving mean")) self.comboBox.setItemText(1, _translate("SmoothDialog", "Savitzky-Golay")) @@ -117,5 +123,4 @@ class Ui_SmoothDialog(object): self.comboBox.setItemText(6, _translate("SmoothDialog", "Moving maximum")) self.comboBox.setItemText(7, _translate("SmoothDialog", "Moving minimum")) self.comboBox.setItemText(8, _translate("SmoothDialog", "Moving sum")) - self.y_checkBox.setText(_translate("SmoothDialog", "y log-spaced?")) - self.x_checkBox.setText(_translate("SmoothDialog", "x log-spaced?")) + self.label_2.setText(_translate("SmoothDialog", "

Note: Sets must be sorted for correct results

")) diff --git a/src/resources/_ui/smoothdialog.ui b/src/resources/_ui/smoothdialog.ui index e26b521..19e63ec 100644 --- a/src/resources/_ui/smoothdialog.ui +++ b/src/resources/_ui/smoothdialog.ui @@ -17,7 +17,20 @@ 3 - + + + + Qt::Vertical + + + + 20 + 40 + + + + + Window length @@ -27,68 +40,14 @@ - - - - - 3 - - - 0 - - - 0 - - - 0 - - - 0 - - - - - Iterations - - - iter_spinBox - - - - - - - 3 - - - 1 - - - 1 - - - - - - - + Qt::Horizontal - - - - Qt::Horizontal - - - QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - + @@ -132,7 +91,65 @@ - + + + + y log-spaced? + + + + + + + x log-spaced? + + + + + + + + 3 + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Iterations + + + iter_spinBox + + + + + + + 3 + + + 1 + + + 1 + + + + + + + <html><head/><body><p>Number of data points used as smoothing window.</p></body></html> @@ -145,7 +162,17 @@ - + + + + Qt::Horizontal + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + @@ -194,30 +221,13 @@ - - - - Qt::Vertical - - - - 20 - 40 - - - - - - + + - y log-spaced? + <html><head/><body><p><span style=" font-weight:600;">Note:</span> Sets must be sorted for correct results</p></body></html> - - - - - - x log-spaced? + + 3 From 465fb0c09a67ee3e07e325472062655056049ec2 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 18 Jan 2024 18:25:07 +0000 Subject: [PATCH 04/30] 194-fitrange (#219) keyboard-setting of custom fit range; closes #194; helps for #32 --- src/gui_qt/_py/basewindow.py | 13 +- src/gui_qt/_py/fitmodelwidget.py | 9 +- src/gui_qt/fit/fit_forms.py | 176 +++++----------------------- src/gui_qt/fit/fit_parameter.py | 160 ++++++++++++++++++++++--- src/gui_qt/fit/fit_toolbar.py | 97 +++++++++++++++ src/gui_qt/fit/fitfunction.py | 6 +- src/gui_qt/fit/fitwindow.py | 13 +- src/gui_qt/fit/result.py | 2 +- src/gui_qt/lib/namespace.py | 80 +++++++------ src/gui_qt/main/mainwindow.py | 37 ++---- src/resources/_ui/basewindow.ui | 24 ---- src/resources/_ui/fitmodelwidget.ui | 25 ++++ 12 files changed, 368 insertions(+), 274 deletions(-) create mode 100644 src/gui_qt/fit/fit_toolbar.py diff --git a/src/gui_qt/_py/basewindow.py b/src/gui_qt/_py/basewindow.py index 5c0bff4..3b24795 100644 --- a/src/gui_qt/_py/basewindow.py +++ b/src/gui_qt/_py/basewindow.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'resources/_ui/basewindow.ui' +# Form implementation generated from reading ui file 'src/resources/_ui/basewindow.ui' # # Created by: PyQt5 UI code generator 5.15.10 # @@ -153,15 +153,6 @@ class Ui_BaseWindow(object): self.toolBar_nmr.setIconSize(QtCore.QSize(24, 24)) self.toolBar_nmr.setObjectName("toolBar_nmr") BaseWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar_nmr) - self.toolBar_fit = QtWidgets.QToolBar(BaseWindow) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.toolBar_fit.sizePolicy().hasHeightForWidth()) - self.toolBar_fit.setSizePolicy(sizePolicy) - self.toolBar_fit.setIconSize(QtCore.QSize(24, 24)) - self.toolBar_fit.setObjectName("toolBar_fit") - BaseWindow.addToolBar(QtCore.Qt.TopToolBarArea, self.toolBar_fit) self.toolBar_spectrum = QtWidgets.QToolBar(BaseWindow) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) sizePolicy.setHorizontalStretch(0) @@ -496,7 +487,6 @@ class Ui_BaseWindow(object): self.toolbar_edit.addAction(self.actionShift) self.toolBar_nmr.addAction(self.t1action) self.toolBar_nmr.addAction(self.actionCalculateT1) - self.toolBar_fit.addAction(self.action_FitWidget) self.toolBar_spectrum.addAction(self.action_edit) self.toolBar_spectrum.addAction(self.actionPick_position) self.toolBar_data.addAction(self.actionConcatenate_sets) @@ -537,7 +527,6 @@ class Ui_BaseWindow(object): self.toolBar.setWindowTitle(_translate("BaseWindow", "Main")) self.toolbar_edit.setWindowTitle(_translate("BaseWindow", "Math")) self.toolBar_nmr.setWindowTitle(_translate("BaseWindow", "NMR")) - self.toolBar_fit.setWindowTitle(_translate("BaseWindow", "Fit")) self.toolBar_spectrum.setWindowTitle(_translate("BaseWindow", "Spectrum")) self.toolBar_data.setWindowTitle(_translate("BaseWindow", "Data")) self.action_close.setText(_translate("BaseWindow", "&Quit")) diff --git a/src/gui_qt/_py/fitmodelwidget.py b/src/gui_qt/_py/fitmodelwidget.py index f183f36..8664646 100644 --- a/src/gui_qt/_py/fitmodelwidget.py +++ b/src/gui_qt/_py/fitmodelwidget.py @@ -2,7 +2,7 @@ # Form implementation generated from reading ui file 'src/resources/_ui/fitmodelwidget.ui' # -# Created by: PyQt5 UI code generator 5.15.9 +# Created by: PyQt5 UI code generator 5.15.10 # # 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. @@ -42,6 +42,7 @@ class Ui_FitParameter(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.parameter_line.sizePolicy().hasHeightForWidth()) self.parameter_line.setSizePolicy(sizePolicy) + self.parameter_line.setMaximumSize(QtCore.QSize(160, 16777215)) self.parameter_line.setText("") self.parameter_line.setObjectName("parameter_line") self.horizontalLayout_2.addWidget(self.parameter_line) @@ -51,6 +52,9 @@ class Ui_FitParameter(object): self.global_checkbox = QtWidgets.QCheckBox(FitParameter) self.global_checkbox.setObjectName("global_checkbox") self.horizontalLayout_2.addWidget(self.global_checkbox) + self.reset_button = QtWidgets.QPushButton(FitParameter) + self.reset_button.setObjectName("reset_button") + self.horizontalLayout_2.addWidget(self.reset_button) self.verticalLayout.addLayout(self.horizontalLayout_2) self.frame = QtWidgets.QFrame(FitParameter) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum) @@ -82,6 +86,7 @@ class Ui_FitParameter(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.lineEdit.sizePolicy().hasHeightForWidth()) self.lineEdit.setSizePolicy(sizePolicy) + self.lineEdit.setMaximumSize(QtCore.QSize(100, 16777215)) self.lineEdit.setText("") self.lineEdit.setFrame(True) self.lineEdit.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) @@ -100,6 +105,7 @@ class Ui_FitParameter(object): sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(self.lineEdit_2.sizePolicy().hasHeightForWidth()) self.lineEdit_2.setSizePolicy(sizePolicy) + self.lineEdit_2.setMaximumSize(QtCore.QSize(100, 16777215)) self.lineEdit_2.setText("") self.lineEdit_2.setFrame(True) self.lineEdit_2.setObjectName("lineEdit_2") @@ -122,6 +128,7 @@ class Ui_FitParameter(object): self.parameter_line.setPlaceholderText(_translate("FitParameter", "0")) self.fixed_check.setText(_translate("FitParameter", "Fix")) self.global_checkbox.setText(_translate("FitParameter", "Global")) + self.reset_button.setText(_translate("FitParameter", "Use global")) self.lineEdit.setToolTip(_translate("FitParameter", "

Lower bound. Same bound is used for all data. Leave empty for no boundary condition.

")) self.label_3.setText(_translate("FitParameter", "Textlabel")) self.lineEdit_2.setToolTip(_translate("FitParameter", "

Upper bound. Same bound is used for all data. Leave empty for no boundary condition.

")) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index 4b696c5..60c3191 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -1,138 +1,11 @@ from __future__ import annotations -from nmreval.utils.text import convert - from ..Qt import QtCore, QtWidgets, QtGui -from .._py.fitmodelwidget import Ui_FitParameter from .._py.save_fitmodel_dialog import Ui_SaveDialog from ..lib.iconloading import get_icon from ..lib.tables import TableWidget -class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): - """ - Widget to show a global parameter - """ - - value_requested = QtCore.pyqtSignal(object) - value_changed = QtCore.pyqtSignal(str) - state_changed = QtCore.pyqtSignal() - replace_single_value = QtCore.pyqtSignal(object) - - def __init__(self, label: str = 'Fitparameter', parent=None, fixed: bool = False): - super().__init__(parent) - self.setupUi(self) - - self.name = label - - self.parametername.setText(convert(label) + ' ') - - self.parameter_line.setText('1') - self.parameter_line.setMaximumWidth(160) - self.lineEdit.setMaximumWidth(100) - self.lineEdit_2.setMaximumWidth(100) - - self.label_3.setText(f'< {convert(label)} <') - - self.checkBox.stateChanged.connect(self.enableBounds) - self.global_checkbox.stateChanged.connect(lambda: self.state_changed.emit()) - self.parameter_line.editingFinished.connect(self.update_parameter) - self.parameter_line.values_requested.connect(lambda: self.value_requested.emit(self)) - self.parameter_line.replace_single_values.connect(lambda: self.replace_single_value.emit(None)) - self.parameter_line.editingFinished.connect(lambda: self.value_changed.emit(self.parameter_line.text())) - self.fixed_check.toggled.connect(self.set_fixed) - - if fixed: - self.fixed_check.hide() - - self.parameter_pos = None - self.func_idx = None - - self._linetext = '1' - - self.menu = QtWidgets.QMenu(self) - - def set_parameter_string(self, p: str): - self.parameter_line.setText(p) - self.parameter_line.setToolTip(p) - - def set_bounds(self, lb: float, ub: float, cbox: bool = True): - self.checkBox.setCheckState(QtCore.Qt.Checked if cbox else QtCore.Qt.Unchecked) - for val, bds_line in [(lb, self.lineEdit), (ub, self.lineEdit_2)]: - if val is not None: - bds_line.setText(str(val)) - else: - bds_line.setText('') - - def enableBounds(self, value: int): - self.lineEdit.setEnabled(value == 2) - self.lineEdit_2.setEnabled(value == 2) - - def set_parameter(self, p: float | None, bds: tuple[float, float, bool] = None, - fixed: bool = None, glob: bool = None): - ptext = f'{p:.4g}' - - self.set_parameter_string(ptext) - - if bds is not None: - self.set_bounds(*bds) - - if fixed is not None: - self.fixed_check.setCheckState(QtCore.Qt.CheckState.Unchecked if fixed else QtCore.Qt.CheckState.Checked) - - if glob is not None: - self.global_checkbox.setCheckState(QtCore.Qt.CheckState.Checked if glob else QtCore.Qt.CheckState.Unchecked) - - def get_parameter(self): - try: - p = float(self.parameter_line.text().replace(',', '.')) - except ValueError: - p = self.parameter_line.text().replace(',', '.') - - if self.checkBox.isChecked(): - lb_text = self.lineEdit.text() - lb = None - if lb_text: - try: - lb = float(lb_text.replace(',', '.')) - except ValueError: - lb = lb_text - - ub_text = self.lineEdit_2.text() - rb = None - if ub_text: - try: - rb = float(ub_text.replace(',', '.')) - except ValueError: - rb = ub_text - else: - lb = rb = None - - bounds = (lb, rb) - - return p, bounds, not self.fixed_check.isChecked(), self.global_checkbox.isChecked() - - @QtCore.pyqtSlot(bool) - def set_fixed(self, state: bool): - # self.global_checkbox.setVisible(not state) - self.frame.setVisible(not state) - - @QtCore.pyqtSlot() - def update_parameter(self): - new_value = self.parameter_line.text() - if not new_value: - self.parameter_line.setText('1') - - try: - float(new_value) - is_text = False - except ValueError: - is_text = True - self.global_checkbox.setCheckState(False) - - self.set_fixed(is_text or self.fixed_check.isChecked()) - - class QSaveModelDialog(QtWidgets.QDialog, Ui_SaveDialog): def __init__(self, types=None, parent=None): super().__init__(parent=parent) @@ -172,30 +45,37 @@ class FitModelTree(QtWidgets.QTreeWidget): treeChanged = QtCore.pyqtSignal() itemRemoved = QtCore.pyqtSignal(int) - counterRole = QtCore.Qt.UserRole + 1 - operatorRole = QtCore.Qt.UserRole + 2 + counterRole = QtCore.Qt.ItemDataRole.UserRole + 1 + operatorRole = QtCore.Qt.ItemDataRole.UserRole + 2 def __init__(self, parent=None): super().__init__(parent=parent) self.setHeaderHidden(True) self.setDragEnabled(True) self.setDragDropMode(QtWidgets.QTreeWidget.InternalMove) - self.setDefaultDropAction(QtCore.Qt.MoveAction) + self.setDefaultDropAction(QtCore.Qt.DropAction.MoveAction) self.itemSelectionChanged.connect(lambda: self.treeChanged.emit()) def keyPressEvent(self, evt): - operators = [QtCore.Qt.Key_Plus, QtCore.Qt.Key_Asterisk, - QtCore.Qt.Key_Minus, QtCore.Qt.Key_Slash] + operators = [ + QtCore.Qt.Key.Key_Plus, + QtCore.Qt.Key.Key_Asterisk, + QtCore.Qt.Key.Key_Minus, + QtCore.Qt.Key.Key_Slash + ] - if evt.key() == QtCore.Qt.Key_Delete: + if evt.key() == QtCore.Qt.Key.Key_Delete: for item in self.selectedItems(): self.remove_function(item) - elif evt.key() == QtCore.Qt.Key_Space: + elif evt.key() == QtCore.Qt.Key.Key_Space: for item in self.treeWidget.selectedItems(): - item.setCheckState(0, QtCore.Qt.Checked) if item.checkState( - 0) == QtCore.Qt.Unchecked else item.setCheckState(0, QtCore.Qt.Unchecked) + cs = item.checkState(0) + if cs == QtCore.Qt.CheckState.Unchecked: + item.setCheckState(0, QtCore.Qt.CheckState.Checked) + else: + item.setCheckState(0, QtCore.Qt.CheckState.Unchecked) elif evt.key() in operators: idx = operators.index(evt.key()) @@ -246,7 +126,7 @@ class FitModelTree(QtWidgets.QTreeWidget): color = QtGui.QColor(color) it = QtWidgets.QTreeWidgetItem() - it.setData(0, QtCore.Qt.UserRole, idx) + it.setData(0, QtCore.Qt.ItemDataRole.UserRole, idx) it.setData(0, self.counterRole, cnt) it.setData(0, self.operatorRole, op) it.setText(0, name) @@ -257,7 +137,7 @@ class FitModelTree(QtWidgets.QTreeWidget): it.setForeground(0, QtGui.QBrush(color)) it.setIcon(0, get_icon(self.icons[op])) - it.setCheckState(0, QtCore.Qt.Checked if active else QtCore.Qt.Unchecked) + it.setCheckState(0, QtCore.Qt.CheckState.Checked if active else QtCore.Qt.CheckState.Unchecked) if parent is None: self.addTopLevelItem(it) @@ -277,7 +157,7 @@ class FitModelTree(QtWidgets.QTreeWidget): def get_selected(self): try: it = self.selectedItems()[0] - function_nr = it.data(0, QtCore.Qt.UserRole) + function_nr = it.data(0, QtCore.Qt.ItemDataRole.UserRole) idx = it.data(0, self.counterRole) except IndexError: @@ -300,10 +180,10 @@ class FitModelTree(QtWidgets.QTreeWidget): it = parent.child(i) child = { - 'idx': it.data(0, QtCore.Qt.UserRole), + 'idx': it.data(0, QtCore.Qt.ItemDataRole.UserRole), 'op': it.data(0, self.operatorRole), 'pos': pos, - 'active': (it.checkState(0) == QtCore.Qt.Checked), + 'active': (it.checkState(0) == QtCore.Qt.CheckState.Checked), 'children': [] } @@ -371,8 +251,8 @@ class FitTableWidget(TableWidget): for (sid, name) in set_ids: item = QtWidgets.QTableWidgetItem(name) - item.setCheckState(QtCore.Qt.Checked) - item.setData(QtCore.Qt.UserRole+1, sid) + item.setCheckState(QtCore.Qt.CheckState.Checked) + item.setData(QtCore.Qt.ItemDataRole.UserRole+1, sid) row = self.rowCount() self.setRowCount(row+1) self.setItem(row, 0, item) @@ -390,15 +270,15 @@ class FitTableWidget(TableWidget): for i in range(self.rowCount()): item = self.item(i, 0) - if item.checkState() == QtCore.Qt.Checked: + if item.checkState() == QtCore.Qt.CheckState.Checked: mod = self.cellWidget(i, 1).currentData() if mod is None: mod = default if include_name: - arg = (item.data(QtCore.Qt.UserRole+1), item.text()) + arg = (item.data(QtCore.Qt.ItemDataRole.UserRole+1), item.text()) else: - arg = item.data(QtCore.Qt.UserRole+1) + arg = item.data(QtCore.Qt.ItemDataRole.UserRole+1) if mod not in data: data[mod] = [] @@ -411,8 +291,8 @@ class FitTableWidget(TableWidget): for i in range(self.rowCount()): item = self.item(i, 0) if include_name: - ret_val.append((item.data(QtCore.Qt.UserRole+1), item.text())) + ret_val.append((item.data(QtCore.Qt.ItemDataRole.UserRole+1), item.text())) else: - ret_val.append(item.data(QtCore.Qt.UserRole+1)) + ret_val.append(item.data(QtCore.Qt.ItemDataRole.UserRole+1)) return ret_val diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index e4a72d2..e470691 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -1,14 +1,12 @@ from __future__ import annotations -from typing import Optional - from nmreval.fit.parameter import Parameter from nmreval.utils.text import convert from ..Qt import QtWidgets, QtCore, QtGui from .._py.fitfuncwidget import Ui_FormFit +from .._py.fitmodelwidget import Ui_FitParameter from ..lib.forms import SelectionWidget -from .fit_forms import FitModelWidget class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): @@ -30,16 +28,15 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): self.scrollwidget2.setLayout(QtWidgets.QVBoxLayout()) def eventFilter(self, src: QtCore.QObject, evt: QtCore.QEvent): + modifiers = QtCore.Qt.KeyboardModifier.ControlModifier | QtCore.Qt.KeyboardModifier.ShiftModifier if isinstance(evt, QtGui.QKeyEvent): - if (evt.key() == QtCore.Qt.Key_Right) and \ - (evt.modifiers() == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier): + if (evt.key() == QtCore.Qt.Key.Key_Right) and (evt.modifiers() == modifiers): self.change_single_parameter(src.value, sender=src) self.select_next_preview(1) return True - elif (evt.key() == QtCore.Qt.Key_Left) and \ - (evt.modifiers() == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier): + elif (evt.key() == QtCore.Qt.Key.Key_Left) and (evt.modifiers() == modifiers): self.change_single_parameter(src.value, sender=src) self.select_next_preview(-1) @@ -65,7 +62,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): self.glob_values = [1] * len(func.params) for k, v in enumerate(func.params): - widgt = FitModelWidget(label=v, parent=self.scrollwidget) + widgt = ParameterGlobalWidget(name=v, parent=self.scrollwidget) widgt.parameter_pos = k widgt.func_idx = idx try: @@ -95,7 +92,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): for w1, w2 in zip(self.global_parameter, self.data_parameter): w1.parametername.setFixedSize(self.max_width) w1.checkBox.setFixedSize(self.max_width) - w2.label.setFixedSize(self.max_width) + w2.parametername.setFixedSize(self.max_width) if hasattr(func, 'choices') and func.choices is not None: cbox = func.choices @@ -175,7 +172,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): # disable single parameter if it is set global, enable if global is unset widget = self.sender() idx = self.global_parameter.index(widget) - enable = (widget.global_checkbox.checkState() == QtCore.Qt.Unchecked) + enable = (widget.global_checkbox.checkState() == QtCore.Qt.CheckState.Unchecked) self.data_parameter[idx].setEnabled(enable) def select_next_preview(self, direction): @@ -215,7 +212,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): param_general = [] for g in self.global_parameter: - if isinstance(g, FitModelWidget): + if isinstance(g, ParameterGlobalWidget): p_i, bds_i, fixed_i, global_i = g.get_parameter() parameter_i = Parameter(name=g.name, value=p_i, lb=bds_i[0], ub=bds_i[1], var=fixed_i) param_general.append(parameter_i) @@ -236,7 +233,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): p = [] for i, (p_i, g) in enumerate(zip(parameter, self.global_parameter)): - if isinstance(g, FitModelWidget): + if isinstance(g, ParameterGlobalWidget): if (p_i is None) or is_global[i]: # set has no oen value p.append(param_general[i].copy()) @@ -303,8 +300,8 @@ class ParameterSingleWidget(QtWidgets.QWidget): self._init_ui() self.name = name - self.label.setText(convert(name)) - self.label.setToolTip('If this is bold then this parameter is only for this data. ' + self.parametername.setText(convert(name)) + self.parametername.setToolTip('If this is bold then this parameter is only for this data. ' 'Otherwise, the general parameter is used and displayed') # self.value_line.setValidator(QtGui.QDoubleValidator()) @@ -316,8 +313,8 @@ class ParameterSingleWidget(QtWidgets.QWidget): layout.setContentsMargins(2, 2, 2, 2) layout.setSpacing(2) - self.label = QtWidgets.QLabel(self) - layout.addWidget(self.label) + self.parametername = QtWidgets.QLabel(self) + layout.addWidget(self.parametername) layout.addSpacerItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum)) @@ -347,6 +344,133 @@ class ParameterSingleWidget(QtWidgets.QWidget): def show_as_local_parameter(self, is_local: bool): if is_local: - self.label.setStyleSheet('font-weight: bold;') + self.parametername.setStyleSheet('font-weight: bold;') else: - self.label.setStyleSheet('') + self.parametername.setStyleSheet('') + + +class ParameterGlobalWidget(QtWidgets.QWidget, Ui_FitParameter): + """ + Widget to show a global parameter + """ + + value_requested = QtCore.pyqtSignal(object) + value_changed = QtCore.pyqtSignal(str) + state_changed = QtCore.pyqtSignal() + replace_single_value = QtCore.pyqtSignal(object) + + def __init__(self, name: str = 'Fitparameter', parent=None, fixed: bool = False): + super().__init__(parent) + self.setupUi(self) + + self.name = name + self.reset_button.setVisible(False) + + self.parametername.setText(convert(name) + ' ') + + self.parameter_line.setText('1') + self.parameter_line.setMaximumWidth(160) + self.lineEdit.setMaximumWidth(100) + self.lineEdit_2.setMaximumWidth(100) + + self.label_3.setText(f'< {convert(name)} <') + + self.checkBox.stateChanged.connect(self.enableBounds) + self.global_checkbox.stateChanged.connect(lambda: self.state_changed.emit()) + self.parameter_line.editingFinished.connect(self.update_parameter) + self.parameter_line.values_requested.connect(lambda: self.value_requested.emit(self)) + self.parameter_line.replace_single_values.connect(lambda: self.replace_single_value.emit(None)) + self.parameter_line.editingFinished.connect(lambda: self.value_changed.emit(self.parameter_line.text())) + self.fixed_check.toggled.connect(self.set_fixed) + + if fixed: + self.fixed_check.hide() + + self.reset_button.setVisible(False) + + self.parameter_pos = None + self.func_idx = None + + self._linetext = '1' + + self.menu = QtWidgets.QMenu(self) + + def set_parameter_string(self, p: str): + self.parameter_line.setText(p) + self.parameter_line.setToolTip(p) + + def set_bounds(self, lb: float, ub: float, cbox: bool = True): + self.checkBox.setCheckState(QtCore.Qt.CheckState.Checked if cbox else QtCore.Qt.CheckState.Unchecked) + for val, bds_line in [(lb, self.lineEdit), (ub, self.lineEdit_2)]: + if val is not None: + bds_line.setText(str(val)) + else: + bds_line.setText('') + + def enableBounds(self, value: int): + self.lineEdit.setEnabled(value == 2) + self.lineEdit_2.setEnabled(value == 2) + + def set_parameter(self, p: float | None, bds: tuple[float, float, bool] = None, + fixed: bool = None, glob: bool = None): + ptext = f'{p:.4g}' + + self.set_parameter_string(ptext) + + if bds is not None: + self.set_bounds(*bds) + + if fixed is not None: + self.fixed_check.setCheckState(QtCore.Qt.CheckState.Unchecked if fixed else QtCore.Qt.CheckState.Checked) + + if glob is not None: + self.global_checkbox.setCheckState(QtCore.Qt.CheckState.Checked if glob else QtCore.Qt.CheckState.Unchecked) + + def get_parameter(self): + try: + p = float(self.parameter_line.text().replace(',', '.')) + except ValueError: + p = self.parameter_line.text().replace(',', '.') + + if self.checkBox.isChecked(): + lb_text = self.lineEdit.text() + lb = None + if lb_text: + try: + lb = float(lb_text.replace(',', '.')) + except ValueError: + lb = lb_text + + ub_text = self.lineEdit_2.text() + rb = None + if ub_text: + try: + rb = float(ub_text.replace(',', '.')) + except ValueError: + rb = ub_text + else: + lb = rb = None + + bounds = (lb, rb) + + return p, bounds, not self.fixed_check.isChecked(), self.global_checkbox.isChecked() + + @QtCore.pyqtSlot(bool) + def set_fixed(self, state: bool): + # self.global_checkbox.setVisible(not state) + self.frame.setVisible(not state) + + @QtCore.pyqtSlot() + def update_parameter(self): + new_value = self.parameter_line.text() + if not new_value: + self.parameter_line.setText('1') + + try: + float(new_value) + is_text = False + except ValueError: + is_text = True + self.global_checkbox.setCheckState(False) + + self.set_fixed(is_text or self.fixed_check.isChecked()) diff --git a/src/gui_qt/fit/fit_toolbar.py b/src/gui_qt/fit/fit_toolbar.py new file mode 100644 index 0000000..4cfa091 --- /dev/null +++ b/src/gui_qt/fit/fit_toolbar.py @@ -0,0 +1,97 @@ +from ..Qt import QtWidgets, QtGui, QtCore +from ..lib.iconloading import get_icon +from ..lib.pg_objects import RegionItem + + +class FitToolbar(QtWidgets.QToolBar): + def __init__( + self, + fitaction: QtWidgets.QAction, + limit_menu: QtWidgets.QMenu, + parent=None, + ): + super().__init__(parent=parent) + + self.fit_action = fitaction + self.region = RegionItem() + self.addAction(fitaction) + + self.fitlim_button = QtWidgets.QToolButton(self) + self.fitlim_button.setMenu(limit_menu) + self.fitlim_button.setPopupMode(self.fitlim_button.InstantPopup) + self.fitlim_button.setIcon(get_icon('fit_region')) + self.addWidget(self.fitlim_button) + + self.label = QtWidgets.QLabel(self) + self.label.setText('L: ') + self.addWidget(self.label) + self.label.setEnabled(False) + + self.lineedit = QtWidgets.QLineEdit(self) + self.lineedit.setValidator(QtGui.QDoubleValidator()) + self.lineedit.setMaximumWidth(92) + self.addWidget(self.lineedit) + self.lineedit.setEnabled(False) + + self.label2 = QtWidgets.QLabel(self) + self.label2.setText(' R: ') + self.addWidget(self.label2) + self.label2.setEnabled(False) + + self.lineedit2 = QtWidgets.QLineEdit(self) + self.lineedit2.setValidator(QtGui.QDoubleValidator()) + self.addWidget(self.lineedit2) + self.lineedit2.setMaximumWidth(92) + self.lineedit2.setEnabled(False) + + self.limit_group = QtWidgets.QActionGroup(self) + for ac in limit_menu.actions(): + self.limit_group.addAction(ac) + + self.limit_group.triggered.connect(self.change_limit_type) + + self.region.sigRegionChanged.connect(self.change_labels) + self.change_labels() + + self.lineedit.textChanged.connect(self.move_region) + self.lineedit2.textChanged.connect(self.move_region) + + @QtCore.pyqtSlot(QtWidgets.QAction) + def change_limit_type(self, action: QtWidgets.QAction): + is_custom = (action.text() == 'Custom') + + for w in [self.label, self.label2, self.lineedit, self.lineedit2]: + w.setEnabled(is_custom) + + def change_labels(self): + r = self.region.getRegion() + self.lineedit.blockSignals(True) + self.lineedit.setText(f'{r[0]:.4g}') + self.lineedit.blockSignals(False) + + self.lineedit2.blockSignals(True) + self.lineedit2.setText(f'{r[1]:.4g}') + self.lineedit2.blockSignals(False) + + def move_region(self): + try: + r_min = float(self.lineedit.text()) + except ValueError: + r_min = None + + try: + r_max = float(self.lineedit2.text()) + except ValueError: + r_max = None + + if r_min is not None and r_max is not None: + self.region.setRegion((r_min, r_max), use_log=True) + + def get_limit(self): + action_text = self.limit_group.checkedAction().text() + + return { + 'None': 'none', + 'Visible x range': 'x', + 'Custom': self.region.getRegion(), + }[action_text] diff --git a/src/gui_qt/fit/fitfunction.py b/src/gui_qt/fit/fitfunction.py index 54ce567..b524521 100644 --- a/src/gui_qt/fit/fitfunction.py +++ b/src/gui_qt/fit/fitfunction.py @@ -165,7 +165,7 @@ class QFunctionWidget(QtWidgets.QWidget, Ui_Form): self.iscomplex = False while iterator.value(): item = iterator.value() - f = self.functions[item.data(0, QtCore.Qt.UserRole)] + f = self.functions[item.data(0, QtCore.Qt.ItemDataRole.UserRole)] if hasattr(f, 'iscomplex') and f.iscomplex: self.iscomplex = True break @@ -226,7 +226,7 @@ class QFunctionWidget(QtWidgets.QWidget, Ui_Form): iterator = QtWidgets.QTreeWidgetItemIterator(self.functree) while iterator.value(): item = iterator.value() - f = self.functions[item.data(0, QtCore.Qt.UserRole)] + f = self.functions[item.data(0, QtCore.Qt.ItemDataRole.UserRole)] cnt = item.data(0, self.functree.counterRole) all_parameters[f'{f.name}_{cnt}'] = [(convert(pp, new='str'), (cnt, i)) for i, pp in enumerate(f.params)] @@ -240,7 +240,7 @@ class QFunctionWidget(QtWidgets.QWidget, Ui_Form): while iterator.value(): item = iterator.value() if item.checkState(0) != QtCore.Qt.CheckState.Unchecked: - f = self.functions[item.data(0, QtCore.Qt.UserRole)] + f = self.functions[item.data(0, QtCore.Qt.ItemDataRole.UserRole)] if hasattr(f, 'iscomplex') and f.iscomplex: iscomplex = True break diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index ac1f11e..e71e2be 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -9,7 +9,6 @@ import numpy as np from pyqtgraph import mkPen from nmreval.fit._meta import MultiModel, ModelFactory -from nmreval.fit.data import Data from nmreval.fit.model import Model from nmreval.fit.parameter import Parameters from nmreval.fit.result import FitResult @@ -42,8 +41,8 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): self._management = mgmt self._current_model = next(QFitDialog.model_cnt) - self.show_combobox.setItemData(0, self._current_model, QtCore.Qt.UserRole) - self.default_combobox.setItemData(0, self._current_model, QtCore.Qt.UserRole) + self.show_combobox.setItemData(0, self._current_model, QtCore.Qt.ItemDataRole.UserRole) + self.default_combobox.setItemData(0, self._current_model, QtCore.Qt.ItemDataRole.UserRole) self.data_table = FitTableWidget(self.data_widget) self.data_widget.addWidget(self.data_table) @@ -150,9 +149,9 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): # deselect all fit sets for i in range(self.data_table.rowCount()): - data_id = self.data_table.item(i, 0).data(QtCore.Qt.UserRole+1) + data_id = self.data_table.item(i, 0).data(QtCore.Qt.ItemDataRole.UserRole+1) if self._management[data_id].mode == 'fit' or self._management[data_id].has_relation(Relations.isFitPartOf): - self.data_table.item(i, 0).setCheckState(QtCore.Qt.Unchecked) + self.data_table.item(i, 0).setCheckState(QtCore.Qt.CheckState.Unchecked) if self.models: for m in self.models.keys(): @@ -176,7 +175,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): self.default_combobox.addItem('Model '+idx, userData=idx) self.show_combobox.addItem('Model '+idx, userData=idx) - self.show_combobox.setItemData(self.show_combobox.count()-1, idx, QtCore.Qt.UserRole) + self.show_combobox.setItemData(self.show_combobox.count()-1, idx, QtCore.Qt.ItemDataRole.UserRole) self.show_combobox.setCurrentIndex(self.show_combobox.count()-1) self._current_model = idx @@ -190,7 +189,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): self.get_functions() self.functionwidget.clear() - self._current_model = self.show_combobox.itemData(idx, QtCore.Qt.UserRole) + self._current_model = self.show_combobox.itemData(idx, QtCore.Qt.ItemDataRole.UserRole) if self._current_model in self.models and len(self.models[self._current_model]): for el in self.models[self._current_model]: self.functionwidget.add_function(**el) diff --git a/src/gui_qt/fit/result.py b/src/gui_qt/fit/result.py index eeaa56b..17b3799 100644 --- a/src/gui_qt/fit/result.py +++ b/src/gui_qt/fit/result.py @@ -217,8 +217,8 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): if idx in self.graph_opts: view_range, logx, logy = self.graph_opts[idx] - self.fit_plot.setLogMode(x=logx, y=logy) self.fit_plot.setRange(xRange=view_range[0], yRange=view_range[1], padding=0) + self.fit_plot.setLogMode(x=logx, y=logy) self.logx_box.blockSignals(True) self.logx_box.setChecked(logx) self.logx_box.blockSignals(False) diff --git a/src/gui_qt/lib/namespace.py b/src/gui_qt/lib/namespace.py index e6ce2eb..ae783ba 100644 --- a/src/gui_qt/lib/namespace.py +++ b/src/gui_qt/lib/namespace.py @@ -22,51 +22,59 @@ class Namespace: if basic: self.add_namespace( - {'x': (None, 'x values'), - 'y': (None, 'x values'), - 'y_err': (None, 'y error values'), - 'fit': (None, 'dictionary of fit parameter', 'fit["PIKA"]'), - 'np': (np, 'numpy module'), + { + 'x': (None, 'x values'), + 'y': (None, 'x values'), + 'y_err': (None, 'y error values'), + 'fit': (None, 'dictionary of fit parameter', 'fit["PIKA"]'), + 'np': (np, 'numpy module'), }, parents=('Basic', 'General'), ) self.add_namespace( - {'sin': (np.sin, 'Sine', 'sin(PIKA)'), - 'cos': (np.cos, 'Cosine', 'cos(PIKA)'), - 'tan': (np.tan, 'Tangens', 'tan(PIKA)'), - 'ln': (np.log, 'Natural Logarithm', 'ln(PIKA)'), - 'log': (np.log10, 'Logarithm (base 10)', 'log(PIKA)'), - 'exp': (np.exp, 'Exponential', 'exp(PIKA)'), - 'sqrt': (np.sqrt, 'Root', 'sqrt(PIKA)'), - 'lin_range': (np.linspace, 'N evenly spaced over interval [start, stop]', 'lin_range(start, stop, N)'), - 'log_range': (np.geomspace, 'N evenly spaced (log-scale) over interval [start, stop]', 'lin_range(start, stop, N)'), - }, + { + 'sin': (np.sin, 'Sine', 'sin(PIKA)'), + 'cos': (np.cos, 'Cosine', 'cos(PIKA)'), + 'tan': (np.tan, 'Tangens', 'tan(PIKA)'), + 'ln': (np.log, 'Natural Logarithm', 'ln(PIKA)'), + 'log': (np.log10, 'Logarithm (base 10)', 'log(PIKA)'), + 'exp': (np.exp, 'Exponential', 'exp(PIKA)'), + 'sqrt': (np.sqrt, 'Root', 'sqrt(PIKA)'), + 'lin_range': (np.linspace, 'N evenly spaced over interval [start, stop]', 'lin_range(start, stop, N)'), + 'log_range': (np.geomspace, 'N evenly spaced (log-scale) over interval [start, stop]', 'lin_range(start, stop, N)'), + }, parents=('Basic', 'Functions')) self.add_namespace( - {'max': (np.max, 'Maximum value', 'max(PIKA)'), - 'min': (np.min, 'Minimum value', 'min(PIKA)'), - 'argmax': (np.argmax, 'Index of maximum value', 'argmax(PIKA)'), - 'argmin': (np.argmax, 'Index of minimum value', 'argmin(PIKA)'), - }, + { + 'max': (np.max, 'Maximum value', 'max(PIKA)'), + 'min': (np.min, 'Minimum value', 'min(PIKA)'), + 'argmax': (np.argmax, 'Index of maximum value', 'argmax(PIKA)'), + 'argmin': (np.argmax, 'Index of minimum value', 'argmin(PIKA)'), + }, parents=('Basic', 'Values')), if const: self.add_namespace( - {'e': (constants.e, 'e / As'), - 'eps0': (constants.epsilon0, 'epsilon0 / As/Vm'), - 'Eu': (constants.Eu,), 'h': (constants.h, 'h / eVs'), - 'hbar': (constants.hbar, 'hbar / eVs'), 'kB': (constants.kB, 'kB / eV/K'), - 'mu0': (constants.mu0, 'mu0 / Vs/Am'), 'NA': (constants.NA, 'NA / 1/mol'), - 'pi': (constants.pi,), 'R': (constants.R, 'R / eV'), - }, + { + 'e': (constants.e, 'e / As'), + 'eps0': (constants.epsilon0, 'epsilon0 / As/Vm'), + 'Eu': (constants.Eu,), + 'h': (constants.h, 'h / eVs'), + 'hbar': (constants.hbar, 'hbar / eVs'), + 'kB': (constants.kB, 'kB / eV/K'), + 'mu0': (constants.mu0, 'mu0 / Vs/Am'), + 'NA': (constants.NA, 'NA / 1/mol'), + 'pi': (constants.pi,), + 'R': (constants.R, 'R / eV'), + }, parents=('Constants', 'Maybe useful'), ) self.add_namespace( {f'gamma["{k}"]': (v, k, f'gamma["{k}"]') for k, v in constants.gamma.items()}, - parents=('Constants', 'Magnetogyric ratios (in 1/(sT))') + parents=('Constants', 'Gyromagnetic ratios (in 1/(sT))') ) if fitfuncs: @@ -199,7 +207,7 @@ class QNamespaceWidget(QtWidgets.QWidget, Ui_Form): for entry in subspace: key_item = QtWidgets.QTableWidgetItem(entry) - key_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + key_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) vals = self.namespace.namespace[entry] @@ -214,12 +222,12 @@ class QNamespaceWidget(QtWidgets.QWidget, Ui_Form): display = vals[1] value_item = QtWidgets.QTableWidgetItem(display) - value_item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsEnabled) + value_item.setFlags(QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsEnabled) - key_item.setData(QtCore.Qt.UserRole, alias) - key_item.setData(QtCore.Qt.UserRole+1, entry) - value_item.setData(QtCore.Qt.UserRole, alias) - value_item.setData(QtCore.Qt.UserRole+1, entry) + key_item.setData(QtCore.Qt.ItemDataRole.UserRole, alias) + key_item.setData(QtCore.Qt.ItemDataRole.UserRole+1, entry) + value_item.setData(QtCore.Qt.ItemDataRole.UserRole, alias) + value_item.setData(QtCore.Qt.ItemDataRole.UserRole+1, entry) row = self.namespace_table.rowCount() self.namespace_table.setRowCount(row+1) @@ -241,5 +249,5 @@ class QNamespaceWidget(QtWidgets.QWidget, Ui_Form): @QtCore.pyqtSlot(QtWidgets.QTableWidgetItem, name='on_namespace_table_itemDoubleClicked') def item_selected(self, item: QtWidgets.QTableWidgetItem): - self.selected.emit(item.data(QtCore.Qt.UserRole)) - self.sendKey.emit(item.data(QtCore.Qt.UserRole+1)) + self.selected.emit(item.data(QtCore.Qt.ItemDataRole.UserRole)) + self.sendKey.emit(item.data(QtCore.Qt.ItemDataRole.UserRole+1)) diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 39fdbea..8575319 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -2,7 +2,6 @@ from __future__ import annotations import os import re -import time from pathlib import Path from numpy import geomspace, linspace @@ -17,13 +16,13 @@ from ..Qt import QtGui, QtPrintSupport from ..data.shift_graphs import QShift from ..data.signaledit import QPreviewDialog, QBaselineDialog from ..dsc.glass_dialog import TgCalculator +from ..fit.fit_toolbar import FitToolbar from ..fit.result import FitExtension, QFitResult from ..graphs.graphwindow import QGraphWindow from ..graphs.movedialog import QMover from ..io.fcbatchreader import QFCReader from ..io.filedialog import * from ..lib.iconloading import make_action_icons, get_icon -from ..lib.pg_objects import RegionItem from ..lib.starter import make_starter from ..math.binning import BinningWindow from ..math.evaluation import QEvalDialog @@ -97,12 +96,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.norm_toolbutton.setIcon(get_icon('normal')) self.toolbar_edit.addWidget(self.norm_toolbutton) - self.fitlim_button = QtWidgets.QToolButton(self) - self.fitlim_button.setMenu(self.menuLimits) - self.fitlim_button.setPopupMode(self.fitlim_button.InstantPopup) - self.fitlim_button.setIcon(get_icon('fit_region')) - self.toolBar_fit.addWidget(self.fitlim_button) - while self.tabWidget.count() > 2: self.tabWidget.removeTab(self.tabWidget.count()-1) @@ -120,9 +113,11 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): # noinspection PyUnresolvedReferences self.statusBar.addWidget(self.mousepos) - self.fitregion = RegionItem() self._fit_plot_id = None + self.fit_toolbar = FitToolbar(self.action_FitWidget, self.menuLimits, self) + self.addToolBar(self.fit_toolbar) + self.setGeometry(QtWidgets.QStyle.alignedRect( QtCore.Qt.LayoutDirection.LeftToRight, QtCore.Qt.AlignmentFlag.AlignCenter, @@ -138,11 +133,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.ac_group.addAction(self.action_nm_fit) self.ac_group.addAction(self.action_odr_fit) - self.ac_group2 = QtWidgets.QActionGroup(self) - self.ac_group2.addAction(self.action_no_range) - self.ac_group2.addAction(self.action_x_range) - self.ac_group2.addAction(self.action_custom_range) - def _init_signals(self): self.actionRedo = self.management.undostack.createRedoAction(self) icon = QtGui.QIcon.fromTheme("edit-redo") @@ -158,7 +148,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.action_save_fit_parameter.triggered.connect(self.save_fit_parameter) # noinspection PyUnresolvedReferences - self.ac_group2.triggered.connect(self.change_fit_limits) + self.fit_toolbar.limit_group.triggered.connect(self.change_fit_limits) self.t1action.triggered.connect(lambda: self._show_tab('t1_temp')) self.action_edit.triggered.connect(self.do_preview) @@ -899,29 +889,28 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): for item in self.fit_dialog.preview_lines: self.current_graph_widget.add_external(item) if self.action_custom_range.isChecked(): - self.current_graph_widget.add_external(self.fitregion) + self.current_graph_widget.add_external(self.fit_toolbar.region) block_window = True else: for item in self.fit_dialog.preview_lines: self.current_graph_widget.remove_external(item) - self.current_graph_widget.remove_external(self.fitregion) + self.current_graph_widget.remove_external(self.fit_toolbar.region) return block_window @QtCore.pyqtSlot(QtWidgets.QAction) def change_fit_limits(self, action: QtWidgets.QAction): + if self.current_graph_widget is None: + return + if action == self.action_custom_range and self.fit_dialog.isVisible(): - self.current_graph_widget.add_external(self.fitregion) + self.current_graph_widget.add_external(self.fit_toolbar.region) else: - self.current_graph_widget.remove_external(self.fitregion) + self.current_graph_widget.remove_external(self.fit_toolbar.region) def start_fit(self, parameter, links, fit_options): - fit_options['limits'] = { - self.action_no_range: 'none', - self.action_x_range: 'x', - self.action_custom_range: self.fitregion.getRegion() - }[self.ac_group2.checkedAction()] + fit_options['limits'] = self.fit_toolbar.get_limit() fit_options['fit_mode'] = { self.action_lm_fit: 'lsq', diff --git a/src/resources/_ui/basewindow.ui b/src/resources/_ui/basewindow.ui index 8a5efaf..bf3de79 100644 --- a/src/resources/_ui/basewindow.ui +++ b/src/resources/_ui/basewindow.ui @@ -437,30 +437,6 @@
- - - - 0 - 0 - - - - Fit - - - - 24 - 24 - - - - TopToolBarArea - - - false - - - diff --git a/src/resources/_ui/fitmodelwidget.ui b/src/resources/_ui/fitmodelwidget.ui index ffc0b93..d36a6b9 100755 --- a/src/resources/_ui/fitmodelwidget.ui +++ b/src/resources/_ui/fitmodelwidget.ui @@ -67,6 +67,12 @@ 0 + + + 160 + 16777215 + + Initial values @@ -92,6 +98,13 @@
+ + + + Use global + + +
@@ -151,6 +164,12 @@ 0 + + + 100 + 16777215 + + <html><head/><body><p>Lower bound. Same bound is used for all data. Leave empty for no boundary condition.</p></body></html> @@ -195,6 +214,12 @@ 0 + + + 100 + 16777215 + + <html><head/><body><p>Upper bound. Same bound is used for all data. Leave empty for no boundary condition.</p></body></html> From 575cb5e8f6a7d6627371ce63e3153ee5654b3781 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sun, 21 Jan 2024 17:01:46 +0000 Subject: [PATCH 05/30] 209-fit-tree (#222) closes #209 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/222 --- src/gui_qt/fit/fit_forms.py | 2 +- src/gui_qt/fit/fitwindow.py | 4 +-- src/nmreval/fit/_meta.py | 55 +++++++++++++++++++++++++++---------- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index 60c3191..2a9c4a2 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -70,7 +70,7 @@ class FitModelTree(QtWidgets.QTreeWidget): self.remove_function(item) elif evt.key() == QtCore.Qt.Key.Key_Space: - for item in self.treeWidget.selectedItems(): + for item in self.selectedItems(): cs = item.checkState(0) if cs == QtCore.Qt.CheckState.Unchecked: item.setCheckState(0, QtCore.Qt.CheckState.Checked) diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index e71e2be..a5a4fd0 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -275,7 +275,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): func_dict = {} for model_name, model_parameter in self.models.items(): - func, order, param_len = ModelFactory.create_from_list(model_parameter) + func, order, param_len, _ = ModelFactory.create_from_list(model_parameter) multiple_funcs = isinstance(func, MultiModel) if func is None: continue @@ -387,7 +387,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): func_dict = {} for k, mod in self.models.items(): - func, order, param_len = ModelFactory.create_from_list(mod) + func, order, param_len, _ = ModelFactory.create_from_list(mod) multiple_funcs = isinstance(func, MultiModel) if k in data: diff --git a/src/nmreval/fit/_meta.py b/src/nmreval/fit/_meta.py index 889cc57..ea33d06 100644 --- a/src/nmreval/fit/_meta.py +++ b/src/nmreval/fit/_meta.py @@ -9,7 +9,13 @@ from inspect import signature, Parameter class ModelFactory: @staticmethod - def create_from_list(funcs: list, left=None, func_order=None, param_len=None, left_cnt=None): + def create_from_list( + funcs: list, + left=None, + func_order: list[int] = None, + param_len: list[int] = None, + left_cnt: int = 0, + ): if func_order is None: func_order = [] @@ -20,32 +26,50 @@ class ModelFactory: if not func['active']: continue - func_order.append(func['cnt']) - param_len.append(len(func['func'].params)) - if func['children']: - right, _, _ = ModelFactory.create_from_list(func['children'], left_cnt=func['pos'], - func_order=func_order, param_len=param_len) - right_cnt = None - right = MultiModel(func['func'], right, func['children'][0]['op'], left_idx=func['cnt'], right_idx=None) + f = func.copy() + f['children'] = [] + right, _, _, right_cnt = ModelFactory.create_from_list( + [f] + func['children'], + left_cnt=func['pos'], + func_order=func_order, + param_len=param_len, + ) else: right = func['func'] right_cnt = func['cnt'] + func_order.append(func['cnt']) + param_len.append(len(func['func'].params)) + if left is None: left = right left_cnt = right_cnt else: - left = MultiModel(left, right, func['op'], - left_idx=left_cnt, right_idx=right_cnt) + left = MultiModel(left, right, func['op'], left_idx=left_cnt, right_idx=right_cnt) - return left, func_order, param_len + return left, func_order, param_len, left_cnt class MultiModel: - op_repr = {operator.add: ' + ', operator.mul: ' * ', operator.sub: ' - ', operator.truediv: ' / '} - str_op = {'+': operator.add, '*': operator.mul, '-': operator.sub, '/': operator.truediv} - int_op = {0: operator.add, 1: operator.mul, 2: operator.sub, 3: operator.truediv} + op_repr = { + operator.add: ' + ', + operator.mul: ' * ', + operator.sub: ' - ', + operator.truediv: ' / ', + } + str_op = { + '+': operator.add, + '*': operator.mul, + '-': operator.sub, + '/': operator.truediv, + } + int_op = { + 0: operator.add, + 1: operator.mul, + 2: operator.sub, + 3: operator.truediv, + } def __init__(self, left: Any, @@ -69,6 +93,9 @@ class MultiModel: if self._op is None: raise ValueError('Invalid binary operator.') + if right_idx is None: + right_idx = left_idx + 1 + self.name = '(' self.params = [] self.bounds = [] From 3626cfc7ea7b1c18d080a0896cc66b0e8d873e7d Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 30 Jan 2024 17:01:13 +0000 Subject: [PATCH 06/30] ensure sorted sets before averaging in pick points (#226) should finally fix #189 --- .gitea/ISSUE_TEMPLATE/BUG_TEMPLATE.yml | 2 +- src/nmreval/data/points.py | 39 +++++++++++++++++--------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/.gitea/ISSUE_TEMPLATE/BUG_TEMPLATE.yml b/.gitea/ISSUE_TEMPLATE/BUG_TEMPLATE.yml index c81f466..f7dc7d4 100644 --- a/.gitea/ISSUE_TEMPLATE/BUG_TEMPLATE.yml +++ b/.gitea/ISSUE_TEMPLATE/BUG_TEMPLATE.yml @@ -17,7 +17,7 @@ body: description: For which version have you observed this behavior? placeholder: You find the program version in "Help/About" validations: - required: true + required: false - type: textarea attributes: label: Expected behavior diff --git a/src/nmreval/data/points.py b/src/nmreval/data/points.py index b394efa..8332084 100644 --- a/src/nmreval/data/points.py +++ b/src/nmreval/data/points.py @@ -320,7 +320,11 @@ class Points: pts = [] _tmp_x = self._x[self.mask] - _tmp_y = self._y[self.mask] + x_order = np.argsort(_tmp_x) + _tmp_x = _tmp_x[x_order] + _tmp_y = self._y[self.mask][x_order] + _tmp_yerr = self._y_err[self.mask][x_order] + if idx is not None: for idx_i in idx: if isinstance(idx_i, tuple): @@ -338,7 +342,7 @@ class Points: right_b = int(min(len(self), x_idx + avg_range[1] + 1)) if left_b < right_b: - pts.append([_tmp_x[x_idx], *self._average(avg_mode, x_idx, left_b, right_b)]) + pts.append([_tmp_x[x_idx], *self._average(_tmp_x, _tmp_y, _tmp_yerr, avg_mode, x_idx, left_b, right_b)]) else: pts.append([_tmp_x[x_idx], _tmp_y[x_idx], self._y_err[x_idx]]) @@ -358,28 +362,37 @@ class Points: left_b = int(max(0, x_idx - avg_range[0])) right_b = int(min(len(self), x_idx + avg_range[1] + 1)) - pts.append([_tmp_x[x_idx], *self._average(avg_mode, x_idx, left_b, right_b)]) + pts.append([_tmp_x[x_idx], *self._average(_tmp_x, _tmp_y, _tmp_yerr, avg_mode, x_idx, left_b, right_b)]) return pts - def _average(self, mode: str, idx, left: int, right: int) -> tuple[float, float]: + @staticmethod + def _average( + x: np.ndarray, + y: np.ndarray, + y_err: np.ndarray, + mode: str, + idx: int, + left: int, + right: int, + ) -> tuple[float, float]: if mode == 'mean': - y_mean = np.mean(self._y[self.mask][left:right].real) - y_err = np.linalg.norm(self._y_err[self.mask][left:right]) / (right - left) + y_mean = np.mean(y[left:right].real) + y_err_mean = np.linalg.norm(y_err[left:right]) / (right - left) elif mode == 'sum': - y_mean = np.sum(self._y[self.mask][left:right].real) - y_err = np.linalg.norm(self._y_err[self.mask][left:right]) + y_mean = np.sum(y[left:right].real) + y_err_mean = np.linalg.norm(y_err[left:right]) elif mode == 'integral': - y_mean = simpson(self._y[self.mask][left:right].real, x=self._x[left:right]) - y_err = np.linalg.norm(cumulative_trapezoid(self._y_err[self.mask][left:right].real, x=self._x[left:right])) + y_mean = simpson(y[left:right].real, x=x[left:right]) + y_err_mean = np.linalg.norm(cumulative_trapezoid(y_err[left:right].real, x=x[left:right])) else: - y_mean = self._y[self.mask][idx].real - y_err = self._y_err[self.mask][idx] + y_mean = y[idx].real + y_err_mean = y_err[idx] - return y_mean, y_err + return y_mean, y_err_mean def concatenate(self, other): """ From 813e18a7440c1bf6ba39e13c3f3eccecb0df0668 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 30 Jan 2024 18:01:15 +0000 Subject: [PATCH 07/30] make log-spacing explicit option for custom fit x values (#227) closes #225 --- src/gui_qt/_py/fitresult.py | 95 +++++++++++++++--------- src/gui_qt/fit/result.py | 17 ++++- src/gui_qt/main/mainwindow.py | 3 +- src/gui_qt/main/management.py | 2 +- src/resources/_ui/fitresult.ui | 132 +++++++++++++++++++++------------ 5 files changed, 159 insertions(+), 90 deletions(-) diff --git a/src/gui_qt/_py/fitresult.py b/src/gui_qt/_py/fitresult.py index 54becc9..0f73b79 100644 --- a/src/gui_qt/_py/fitresult.py +++ b/src/gui_qt/_py/fitresult.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'resources/_ui/fitresult.ui' +# Form implementation generated from reading ui file 'src/resources/_ui/fitresult.ui' # # Created by: PyQt5 UI code generator 5.15.10 # @@ -157,67 +157,91 @@ 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 = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, 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) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.graph_checkBox.sizePolicy().hasHeightForWidth()) - self.graph_checkBox.setSizePolicy(sizePolicy) - self.graph_checkBox.setChecked(True) - self.graph_checkBox.setObjectName("graph_checkBox") - self.gridLayout_2.addWidget(self.graph_checkBox, 1, 5, 1, 1) + self.gridLayout_2.addWidget(self.graph_comboBox, 1, 7, 1, 1) self.minx_line = QtWidgets.QLineEdit(self.groupBox) self.minx_line.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, 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.extrapolate_box = QtWidgets.QCheckBox(self.groupBox) + self.extrapolate_box.setObjectName("extrapolate_box") + self.gridLayout_2.addWidget(self.extrapolate_box, 1, 0, 1, 1) + self.numx_line = QtWidgets.QLineEdit(self.groupBox) + self.numx_line.setEnabled(False) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.numx_line.sizePolicy().hasHeightForWidth()) + self.numx_line.setSizePolicy(sizePolicy) + self.numx_line.setObjectName("numx_line") + self.gridLayout_2.addWidget(self.numx_line, 1, 3, 1, 1) + self.graph_checkBox = QtWidgets.QCheckBox(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.graph_checkBox.sizePolicy().hasHeightForWidth()) + self.graph_checkBox.setSizePolicy(sizePolicy) + self.graph_checkBox.setChecked(True) + self.graph_checkBox.setObjectName("graph_checkBox") + self.gridLayout_2.addWidget(self.graph_checkBox, 1, 6, 1, 1) self.maxx_line = QtWidgets.QLineEdit(self.groupBox) self.maxx_line.setEnabled(False) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, 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.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, 5, 2, 1) + self.newx_log_checkbox = QtWidgets.QCheckBox(self.groupBox) + self.newx_log_checkbox.setEnabled(False) + self.newx_log_checkbox.setObjectName("newx_log_checkbox") + self.gridLayout_2.addWidget(self.newx_log_checkbox, 1, 4, 1, 1) self.horizontalLayout = QtWidgets.QHBoxLayout() self.horizontalLayout.setObjectName("horizontalLayout") self.curve_checkbox = QtWidgets.QCheckBox(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.curve_checkbox.sizePolicy().hasHeightForWidth()) + self.curve_checkbox.setSizePolicy(sizePolicy) self.curve_checkbox.setChecked(True) self.curve_checkbox.setObjectName("curve_checkbox") self.horizontalLayout.addWidget(self.curve_checkbox) self.partial_checkBox = QtWidgets.QCheckBox(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.partial_checkBox.sizePolicy().hasHeightForWidth()) + self.partial_checkBox.setSizePolicy(sizePolicy) self.partial_checkBox.setObjectName("partial_checkBox") self.horizontalLayout.addWidget(self.partial_checkBox) - self.gridLayout_2.addLayout(self.horizontalLayout, 0, 0, 1, 4) + self.gridLayout_2.addLayout(self.horizontalLayout, 0, 0, 1, 5) + self.parameter_checkbox = QtWidgets.QCheckBox(self.groupBox) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed) + sizePolicy.setHorizontalStretch(0) + sizePolicy.setVerticalStretch(0) + sizePolicy.setHeightForWidth(self.parameter_checkbox.sizePolicy().hasHeightForWidth()) + self.parameter_checkbox.setSizePolicy(sizePolicy) + self.parameter_checkbox.setObjectName("parameter_checkbox") + self.gridLayout_2.addWidget(self.parameter_checkbox, 0, 6, 1, 2) self.gridLayout.addWidget(self.groupBox, 7, 0, 1, 2) self.buttonBox = QtWidgets.QDialogButtonBox(Dialog) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok|QtWidgets.QDialogButtonBox.Retry) @@ -253,16 +277,17 @@ class Ui_Dialog(object): self.del_prev_checkBox.setText(_translate("Dialog", "Delete previous fits of this set")) self.reject_fit_checkBox.setText(_translate("Dialog", "Reject this fit")) self.groupBox.setTitle(_translate("Dialog", "Output")) - 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.extrapolate_box.setToolTip(_translate("Dialog", "Extrapolates only main function")) + self.extrapolate_box.setText(_translate("Dialog", "Extrapolate curves")) + self.numx_line.setPlaceholderText(_translate("Dialog", "# pts")) + self.graph_checkBox.setText(_translate("Dialog", "New graph for parameter")) 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.newx_log_checkbox.setText(_translate("Dialog", "log-spaced?")) 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")) from ..lib.forms import ElideComboBox from pyqtgraph import GraphicsLayoutWidget diff --git a/src/gui_qt/fit/result.py b/src/gui_qt/fit/result.py index 17b3799..4706cbc 100644 --- a/src/gui_qt/fit/result.py +++ b/src/gui_qt/fit/result.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from math import isnan from pyqtgraph import mkBrush, mkPen @@ -28,6 +30,7 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): 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.extrapolate_box.stateChanged.connect(lambda x: self.newx_log_checkbox.setEnabled(x)) self._previous_fits = {} self._opts = [] @@ -352,7 +355,7 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): parts = self.partial_checkBox.checkState() == QtCore.Qt.CheckState.Checked - extrapolate = [None, None, None] + extrapolate = [None, None, None, None] error = [] if self.extrapolate_box.isChecked(): try: @@ -368,6 +371,8 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): except (TypeError, ValueError): error.append('Number of points is missing') + extrapolate[3] = self.newx_log_checkbox.isChecked() + if error: msg = QtWidgets.QMessageBox.warning(self, 'Error', 'Extrapolation failed because:\n' + '\n'.join(error)) return @@ -405,10 +410,13 @@ class FitExtension(QtWidgets.QDialog): self.num_pts.setValidator(QtGui.QIntValidator()) gridLayout.addWidget(self.num_pts, 2, 1, 1, 1) + self.logx_checkbox = QtWidgets.QCheckBox('Log-spaced?') + gridLayout.addWidget(self.logx_checkbox, 3, 0, 1, 2) + self.buttonBox = QtWidgets.QDialogButtonBox() self.buttonBox.setOrientation(QtCore.Qt.Orientation.Horizontal) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok) - gridLayout.addWidget(self.buttonBox, 3, 0, 1, 2) + gridLayout.addWidget(self.buttonBox, 4, 0, 1, 2) self.setLayout(gridLayout) @@ -416,12 +424,13 @@ class FitExtension(QtWidgets.QDialog): self.buttonBox.rejected.connect(self.reject) @property - def values(self): + def values(self) -> tuple[float, float, int, bool] | None: try: xmin = float(self.min_line.text()) xmax = float(self.max_line.text()) nums = int(self.num_pts.text()) + logx = self.logx_checkbox.isChecked() except TypeError: return None - return xmin, xmax, nums + return xmin, xmax, nums, logx diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 8575319..80935b6 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -984,7 +984,8 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): res = w.exec() if res: p = w.values - x = linspace(p[0], p[1], num=p[2]) + spacefunc = geomspace if p[3] else linspace + x = spacefunc(p[0], p[1], num=p[2]) self.management.extend_fits(sets, x) @QtCore.pyqtSlot(name='on_action_create_fit_function_triggered') diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index 3185b30..b8758b2 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -613,7 +613,7 @@ class UpperManagement(QtCore.QObject): continue if not all(e is None for e in extrapolate): - spacefunc = np.geomspace if fit.islog else np.linspace + spacefunc = np.geomspace if extrapolate[3] else np.linspace xmin = fit.x.min() xmax = fit.x.max() diff --git a/src/resources/_ui/fitresult.ui b/src/resources/_ui/fitresult.ui index 142a794..9f8bd4f 100644 --- a/src/resources/_ui/fitresult.ui +++ b/src/resources/_ui/fitresult.ui @@ -354,59 +354,26 @@ 3 - - - - Extrapolates only main function - - - Extrapolate curves - - - - - - - Plot parameter - - - - + false - + 0 0 - - - - - 0 - 0 - - - - New graph for parameter - - - true - - - false - + 0 0 @@ -419,10 +386,45 @@ - - - - Qt::Vertical + + + + Extrapolates only main function + + + Extrapolate curves + + + + + + + false + + + + 0 + 0 + + + + # pts + + + + + + + + 0 + 0 + + + + New graph for parameter + + + true @@ -432,7 +434,7 @@ false - + 0 0 @@ -445,20 +447,33 @@ - - - - false - - - # pts + + + + Qt::Vertical - + + + + false + + + log-spaced? + + + + + + + 0 + 0 + + Plot fit curve @@ -469,6 +484,12 @@ + + + 0 + 0 + + Plot partial functions @@ -476,6 +497,19 @@ + + + + + 0 + 0 + + + + Plot parameter + + + From 7161a173487981d864d96248ed218b11b33d80e6 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 31 Jan 2024 16:00:07 +0000 Subject: [PATCH 08/30] 228-229-index-problems (#230) bugfixes: closes #228, closes #229 --- src/gui_qt/math/smooth.py | 22 +++++++++++++++++----- src/nmreval/io/asciireader.py | 10 ++++++++-- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/gui_qt/math/smooth.py b/src/gui_qt/math/smooth.py index 387865d..d2ca5e7 100644 --- a/src/gui_qt/math/smooth.py +++ b/src/gui_qt/math/smooth.py @@ -12,10 +12,10 @@ class QSmooth(QtWidgets.QDialog, Ui_SmoothDialog): @QtCore.pyqtSlot(int, name='on_comboBox_currentIndexChanged') def change_mode(self, idx: int): - if idx == 2: + if idx == 1: self.widget.show() self.widget_2.hide() - elif idx == 3: + elif idx == 2: self.widget.show() self.widget_2.show() else: @@ -29,12 +29,24 @@ class QSmooth(QtWidgets.QDialog, Ui_SmoothDialog): idx = self.comboBox.currentIndex() # this order must match the combobox - para['mode'] = ['mean', 'savgol', 'loess', 'median', 'std', 'var', 'max', 'min', 'sum'][idx] + para['mode'] = [ + 'mean', + 'savgol', + 'loess', + 'median', + 'std', + 'var', + 'max', + 'min', + 'sum', + ][idx] - if idx == 2: + # Savitzky-Golay needs also polynomial degree + if idx == 1: para['deg'] = self.polynom_spinBox.value() - if idx == 3: + # LOESS needs also polynomial degree and number of iterations + if idx == 2: para['deg'] = self.polynom_spinBox.value() para['it'] = self.iter_spinBox.value() diff --git a/src/nmreval/io/asciireader.py b/src/nmreval/io/asciireader.py index e8c0b4f..05f84a5 100644 --- a/src/nmreval/io/asciireader.py +++ b/src/nmreval/io/asciireader.py @@ -183,7 +183,13 @@ class AsciiReader: single_len = 2 stepsize = 2 - cls = {'points': Points, 'fid': FID, 'spectrum': Spectrum, 'bds': BDS, 'dsc': DSC}[mode] + cls = { + 'points': Points, + 'fid': FID, + 'spectrum': Spectrum, + 'bds': BDS, + 'dsc': DSC, + }[mode] for j in range(1, num_y+1, stepsize): if col_names is not None: @@ -191,7 +197,7 @@ class AsciiReader: kwargs['name'] = col_names[j-1] elif num_y > single_len: # more than one axis, append column number - kwargs['name'] = filename + '_' + str(y[j-1]) + kwargs['name'] = f'{filename}_{y[j-1]+1}' if j+num_y < raw_data.shape[2]: kwargs['y_err'] = raw_data[i, :, j+num_y] From 39b0fe75cb1375c49aefd0404e0178b2ca15e2cc Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sat, 3 Feb 2024 11:25:35 +0000 Subject: [PATCH 09/30] check relative path; remove possible cause of #231 (#232) Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/232 --- src/gui_qt/io/dscreader.py | 35 +++++++++++++++++++++++++---------- 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/src/gui_qt/io/dscreader.py b/src/gui_qt/io/dscreader.py index b3b0768..a9b83a0 100644 --- a/src/gui_qt/io/dscreader.py +++ b/src/gui_qt/io/dscreader.py @@ -78,14 +78,22 @@ class QDSCReader(QtWidgets.QDialog, Ui_Dialog): for opts in self.sample.steps: item = QtWidgets.QListWidgetItem() - item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsUserCheckable) - item.setCheckState(QtCore.Qt.Unchecked) + item.setFlags( + QtCore.Qt.ItemFlag.ItemIsEnabled | + QtCore.Qt.ItemFlag.ItemIsSelectable | + QtCore.Qt.ItemFlag.ItemIsUserCheckable + ) + item.setCheckState(QtCore.Qt.CheckState.Unchecked) if opts[0] == 'i': - item.setFlags(QtCore.Qt.NoItemFlags) + item.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) item.setText(f'{opts[1]:.2f} K for {opts[2] / 60:.0f} min') else: - item.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsUserCheckable) + item.setFlags( + QtCore.Qt.ItemFlag.ItemIsEnabled | + QtCore.Qt.ItemFlag.ItemIsSelectable | + QtCore.Qt.ItemFlag.ItemIsUserCheckable + ) item.setText(f'{opts[2]:.2f} K to {opts[3]:.2f} K with {opts[1]} K/min') self.step_listWidget.addItem(item) @@ -97,7 +105,12 @@ class QDSCReader(QtWidgets.QDialog, Ui_Dialog): if empty: self.empty = self.calibrator.set_measurement(empty, mode='empty') - self.empty_label.setText('~/' + str(self.empty.fname.relative_to(Path.home()))) + + # avoid ValueError breaking data update + if self.empty.fname.is_relative_to(Path.home()): + self.empty_label.setText('~/' + str(self.empty.fname.relative_to(Path.home()))) + else: + self.empty_label.setText(str(self.empty.fname)) self.update_plots() @@ -118,8 +131,8 @@ class QDSCReader(QtWidgets.QDialog, Ui_Dialog): self.references.append(ref) item = QtWidgets.QTableWidgetItem(str(ref.fname.name)) - item.setData(QtCore.Qt.UserRole, ref.fname) - item.setFlags(QtCore.Qt.ItemIsEnabled) + item.setData(QtCore.Qt.ItemDataRole.UserRole, ref.fname) + item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) rowcnt = self.reference_tableWidget.rowCount() self.reference_tableWidget.setRowCount(rowcnt+1) @@ -132,7 +145,7 @@ class QDSCReader(QtWidgets.QDialog, Ui_Dialog): @QtCore.pyqtSlot(name='on_ref_remove_pushButton_clicked') def remove_reference(self): idx = self.reference_tableWidget.currentRow() - self.calibrator.remove_reference(self.reference_tableWidget.item(idx, 0).data(QtCore.Qt.UserRole)) + self.calibrator.remove_reference(self.reference_tableWidget.item(idx, 0).data(QtCore.Qt.ItemDataRole.UserRole)) self.reference_tableWidget.removeRow(idx) self.update_plots() @@ -145,10 +158,10 @@ class QDSCReader(QtWidgets.QDialog, Ui_Dialog): for row in range(self.step_listWidget.count()): if idx == row: continue - self.step_listWidget.item(row).setCheckState(QtCore.Qt.Unchecked) + self.step_listWidget.item(row).setCheckState(QtCore.Qt.CheckState.Unchecked) self.step_listWidget.blockSignals(False) - if item.checkState() == QtCore.Qt.Checked: + if item.checkState() == QtCore.Qt.CheckState.Checked: mode, rate, _, _ = self.sample.steps[idx] self.current_run = (rate, mode) self.sample_idx = idx @@ -217,6 +230,8 @@ class QDSCReader(QtWidgets.QDialog, Ui_Dialog): if empty_data is not None: self.empty_sample.setData(x=empty_data[0], y=empty_data[1]) + else: + self.empty_sample.setData(x=[], y=[]) self.calib_graph.clear() From 567148b7e656f2e5c35cf366157776b73e399b3c Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 6 Feb 2024 17:23:00 +0000 Subject: [PATCH 10/30] 231-dsc-empty-baseline (#236) closes #231 --- src/gui_qt/dsc/glass_dialog.py | 28 ++++++++++++++-------------- src/gui_qt/io/dscreader.py | 9 +++++++-- src/nmreval/io/dsc.py | 30 ++++++++++++++++++++---------- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/src/gui_qt/dsc/glass_dialog.py b/src/gui_qt/dsc/glass_dialog.py index f815b7a..b7afe09 100644 --- a/src/gui_qt/dsc/glass_dialog.py +++ b/src/gui_qt/dsc/glass_dialog.py @@ -136,8 +136,8 @@ class TgCalculator(QtWidgets.QWizard, Ui_DSCEvalDialog): max_x = max(max_x, data.x.max()) item = QtWidgets.QListWidgetItem(name) - item.setCheckState(QtCore.Qt.Checked) - item.setData(QtCore.Qt.UserRole, key) + item.setCheckState(QtCore.Qt.CheckState.Checked) + item.setData(QtCore.Qt.ItemDataRole.UserRole, key) item.setForeground(mkBrush(c.rgb())) self.listWidget.addItem(item) @@ -191,10 +191,10 @@ class TgCalculator(QtWidgets.QWizard, Ui_DSCEvalDialog): for idx in range(self.listWidget.count()): item = self.listWidget.item(idx) - if item.checkState() == QtCore.Qt.Unchecked: + if item.checkState() == QtCore.Qt.CheckState.Unchecked: continue - key = item.data(QtCore.Qt.UserRole) + key = item.data(QtCore.Qt.ItemDataRole.UserRole) plot = self._plots[key] data, _ = self._dsc[key] @@ -214,7 +214,7 @@ class TgCalculator(QtWidgets.QWizard, Ui_DSCEvalDialog): item = self.listWidget.item(idx) tree_item = QtWidgets.QTreeWidgetItem([item.text()]) - values = self._tg_value.get(item.data(QtCore.Qt.UserRole)) + values = self._tg_value.get(item.data(QtCore.Qt.ItemDataRole.UserRole)) if values is not None: for name, pos in values.items(): @@ -223,7 +223,7 @@ class TgCalculator(QtWidgets.QWizard, Ui_DSCEvalDialog): self.tg_tree.addTopLevelItem(tree_item) - key = item.data(QtCore.Qt.UserRole) + key = item.data(QtCore.Qt.ItemDataRole.UserRole) plot = self._plots[key] data, _ = self._dsc[key] @@ -251,7 +251,7 @@ class TgCalculator(QtWidgets.QWizard, Ui_DSCEvalDialog): @QtCore.pyqtSlot(QtWidgets.QListWidgetItem) def change_visibility(self, item: QtWidgets.QListWidgetItem): is_checked = bool(item.checkState()) - plot = self._plots[item.data(QtCore.Qt.UserRole)] + plot = self._plots[item.data(QtCore.Qt.ItemDataRole.UserRole)] for val in plot: val.setVisible(is_checked) @@ -275,10 +275,10 @@ class TgCalculator(QtWidgets.QWizard, Ui_DSCEvalDialog): self.tnmh_tree.clear() for idx in range(self.listWidget.count()): item = self.listWidget.item(idx) - if item.checkState() == QtCore.Qt.Unchecked: + if item.checkState() == QtCore.Qt.CheckState.Unchecked: continue - key = item.data(QtCore.Qt.UserRole) + key = item.data(QtCore.Qt.ItemDataRole.UserRole) data = self.get_fictive(key, baselines) @@ -292,7 +292,7 @@ class TgCalculator(QtWidgets.QWizard, Ui_DSCEvalDialog): item = self.listWidget.item(idx) tree_item = QtWidgets.QTreeWidgetItem([item.text()]) - values = self._fit.get(item.data(QtCore.Qt.UserRole)) + values = self._fit.get(item.data(QtCore.Qt.ItemDataRole.UserRole)) if values is not None: child_item = QtWidgets.QTreeWidgetItem([values.parameter_string()]) @@ -305,10 +305,10 @@ class TgCalculator(QtWidgets.QWizard, Ui_DSCEvalDialog): ret_dic = {} for idx in range(self.listWidget.count()): item = self.listWidget.item(idx) - if item.checkState() == QtCore.Qt.Unchecked: + if item.checkState() == QtCore.Qt.CheckState.Unchecked: continue - key = item.data(QtCore.Qt.UserRole) + key = item.data(QtCore.Qt.ItemDataRole.UserRole) cp = None if self.fictive_export_check.isChecked(): @@ -332,10 +332,10 @@ class TgCalculator(QtWidgets.QWizard, Ui_DSCEvalDialog): m = [] for idx in range(self.listWidget.count()): item = self.listWidget.item(idx) - if item.checkState() == QtCore.Qt.Unchecked: + if item.checkState() == QtCore.Qt.CheckState.Unchecked: continue - key = item.data(QtCore.Qt.UserRole) + key = item.data(QtCore.Qt.ItemDataRole.UserRole) data, _ = self._dsc[key] try: tg_value = self._tg_value[key][tg_type][0] diff --git a/src/gui_qt/io/dscreader.py b/src/gui_qt/io/dscreader.py index a9b83a0..1ebda46 100644 --- a/src/gui_qt/io/dscreader.py +++ b/src/gui_qt/io/dscreader.py @@ -264,11 +264,16 @@ class QDSCReader(QtWidgets.QDialog, Ui_Dialog): except TypeError: return + if self.cp_checkBox.isChecked() and self.references: + y_label = 'cp' + else: + y_label = 'q' + rate, mode = self.current_run - new_val = DSC(sample_data[0], sample_data[1], value=rate, name=f'{self.fname.stem} {rate} ({mode})') + new_val = DSC(sample_data[0], sample_data[1], value=rate, name=f'{self.fname.stem} {rate}K-min ({mode}, {y_label})') if filesave: - new_val.savetxt(self.fname.with_name(f'{self.fname.stem} {rate}K-min {mode}.dat'.replace(' ', '_'))) + new_val.savetxt(self.fname.with_name(f'{self.fname.stem}_{rate}K-min_{y_label}{mode}.dat'.replace(' ', '_'))) close_after = False else: self.data_read.emit([new_val]) diff --git a/src/nmreval/io/dsc.py b/src/nmreval/io/dsc.py index b531bd4..4aaf606 100644 --- a/src/nmreval/io/dsc.py +++ b/src/nmreval/io/dsc.py @@ -11,7 +11,8 @@ try: from scipy.integrate import simpson except ImportError: from scipy.integrate import simps as simpson -from scipy.interpolate import interp1d +from scipy.interpolate import CubicSpline + ReferenceValue = namedtuple('Reference', ['name', 'transitions']) Cyclohexane = ReferenceValue('Cyclohexane', [(-87.06+273.15, 79.58), (6.54+273.15, None)]) @@ -38,7 +39,7 @@ class DSCSample: def read_file(self, fname: str | Path) -> None: fname = Path(fname) - # file contains weird deg C character in stupiod ISO encoding + # file contains weird deg C character in stupid ISO encoding with fname.open('r', encoding='iso-8859-15') as f: ii = 1 for line in f: @@ -144,9 +145,12 @@ class DSCCalibrator: self.reference = [] self.ref_list = [] - def set_measurement(self, - fname: str | Path | DSCSample, mode: str = 'sample', - reference: ReferenceValue = Cyclohexane): + def set_measurement( + self: DSCCalibrator, + fname: str | Path | DSCSample, + mode: str = 'sample', + reference: ReferenceValue = Cyclohexane + ): if mode not in ['sample', 'empty', 'reference']: raise ValueError(f'Unknown mode {mode}, not "sample", "empty", "reference"') if mode == 'reference' and not isinstance(reference, ReferenceValue): @@ -266,7 +270,12 @@ class DSCCalibrator: return sol - def get_data(self, idx: int, slope: str = 'iso', limits: tuple[float, float] = None): + def get_data( + self: DSCCalibrator, + idx: int, + slope: str = 'iso', + limits: tuple[float, float] = None + ) -> tuple[np.ndarray, np.ndarray, np.ndarray,np.ndarray | None, np.ndarray]: if self.sample.steps[idx][0] == 'i': raise ValueError('baseline correction is not implemented for isotherms') @@ -292,7 +301,7 @@ class DSCCalibrator: empty_y = empty_data[1] if self.sample.length(idx) != self.empty.length(idx_empty): with np.errstate(all='ignore'): - empty_y = interp1d(empty_data[2]-empty_data[2, 0], empty_data[1], fill_value='extrapolate')(sample_data[2, 0]) + empty_y = CubicSpline(empty_data[2]-empty_data[2, 0], empty_data[1], extrapolate=True)(sample_data[2] - sample_data[2, 0]) sample_data[1] -= empty_y drift_value = sample_data.copy()[(2, 1), :] @@ -346,9 +355,10 @@ class DSCCalibrator: offset = region[0, 0] sample_data[1] -= m * (sample_data[2] - region[1, 0]) + offset - line = np.array([[sample_data[2, 0], sample_data[2, -1]], - [m * (sample_data[2, 0] - region[1, 0]) + offset, - m * (sample_data[2, -1] - region[1, 0]) + offset]]) + line = np.array([ + [sample_data[2, 0], sample_data[2, -1]], + [m * (sample_data[2, 0] - region[1, 0]) + offset, m * (sample_data[2, -1] - region[1, 0]) + offset] + ]) else: line = np.array([[sample_data[2, 0], sample_data[2, -1]], [0, 0]]) From 40746bfa7c741126f77335619b0b0aa6ea2b3016 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 7 Feb 2024 17:55:07 +0000 Subject: [PATCH 11/30] add exclude range to fit limits (#237) Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/237 --- src/gui_qt/_py/basewindow.py | 5 +++++ src/gui_qt/fit/fit_toolbar.py | 6 ++++-- src/gui_qt/main/mainwindow.py | 4 ++-- src/gui_qt/main/management.py | 5 ++++- src/resources/_ui/basewindow.ui | 9 +++++++++ 5 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/gui_qt/_py/basewindow.py b/src/gui_qt/_py/basewindow.py index 3b24795..d916548 100644 --- a/src/gui_qt/_py/basewindow.py +++ b/src/gui_qt/_py/basewindow.py @@ -365,6 +365,9 @@ class Ui_BaseWindow(object): self.actionBinning.setObjectName("actionBinning") self.actionTNMH = QtWidgets.QAction(BaseWindow) self.actionTNMH.setObjectName("actionTNMH") + self.actionExclude_region = QtWidgets.QAction(BaseWindow) + self.actionExclude_region.setCheckable(True) + self.actionExclude_region.setObjectName("actionExclude_region") self.menuSave.addAction(self.actionSave) self.menuSave.addAction(self.actionExportGraphic) self.menuSave.addAction(self.action_save_fit_parameter) @@ -419,6 +422,7 @@ class Ui_BaseWindow(object): self.menuLimits.addAction(self.action_no_range) self.menuLimits.addAction(self.action_x_range) self.menuLimits.addAction(self.action_custom_range) + self.menuLimits.addAction(self.actionExclude_region) self.menuFit.addAction(self.action_FitWidget) self.menuFit.addSeparator() self.menuFit.addAction(self.action_create_fit_function) @@ -631,6 +635,7 @@ class Ui_BaseWindow(object): self.actionTNMH_model.setText(_translate("BaseWindow", "Tg , Hodge, TNMH,,,")) self.actionBinning.setText(_translate("BaseWindow", "Binning...")) self.actionTNMH.setText(_translate("BaseWindow", "TNMH...")) + self.actionExclude_region.setText(_translate("BaseWindow", "Exclude region")) from ..data.datawidget.datawidget import DataWidget from ..data.integral_widget import IntegralWidget from ..data.point_select import PointSelectWidget diff --git a/src/gui_qt/fit/fit_toolbar.py b/src/gui_qt/fit/fit_toolbar.py index 4cfa091..283c335 100644 --- a/src/gui_qt/fit/fit_toolbar.py +++ b/src/gui_qt/fit/fit_toolbar.py @@ -58,7 +58,8 @@ class FitToolbar(QtWidgets.QToolBar): @QtCore.pyqtSlot(QtWidgets.QAction) def change_limit_type(self, action: QtWidgets.QAction): - is_custom = (action.text() == 'Custom') + is_custom = (action.text() in ['Custom', 'Exclude region']) + print(is_custom) for w in [self.label, self.label2, self.lineedit, self.lineedit2]: w.setEnabled(is_custom) @@ -93,5 +94,6 @@ class FitToolbar(QtWidgets.QToolBar): return { 'None': 'none', 'Visible x range': 'x', - 'Custom': self.region.getRegion(), + 'Custom': ('in', self.region.getRegion()), + 'Exclude region': ('out', self.region.getRegion()), }[action_text] diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 80935b6..d91b3ac 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -888,7 +888,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.fit_dialog.load(self.management.active_sets) for item in self.fit_dialog.preview_lines: self.current_graph_widget.add_external(item) - if self.action_custom_range.isChecked(): + if self.action_custom_range.isChecked() or self.actionExclude_region.isChecked(): self.current_graph_widget.add_external(self.fit_toolbar.region) block_window = True @@ -904,7 +904,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): if self.current_graph_widget is None: return - if action == self.action_custom_range and self.fit_dialog.isVisible(): + if action in [self.action_custom_range, self.actionExclude_region] and self.fit_dialog.isVisible(): self.current_graph_widget.add_external(self.fit_toolbar.region) else: self.current_graph_widget.remove_external(self.fit_toolbar.region) diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index b8758b2..ab07037 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -511,13 +511,16 @@ class UpperManagement(QtCore.QObject): _x = data_i.x + # options for fit limits 'none', 'x', ('in', custom region), ('out', excluded region) if fit_limits == 'none': inside = slice(None) elif fit_limits == 'x': x_lim, _ = self.graphs[self.current_graph].ranges inside = np.where((_x >= x_lim[0]) & (_x <= x_lim[1])) + elif fit_limits[0] == 'in': + inside = np.where((_x >= fit_limits[1][0]) & (_x <= fit_limits[1][1])) else: - inside = np.where((_x >= fit_limits[0]) & (_x <= fit_limits[1])) + inside = np.where((_x < fit_limits[1][0]) | (_x > fit_limits[1][1])) try: if isinstance(we, str): diff --git a/src/resources/_ui/basewindow.ui b/src/resources/_ui/basewindow.ui index bf3de79..01987b8 100644 --- a/src/resources/_ui/basewindow.ui +++ b/src/resources/_ui/basewindow.ui @@ -247,6 +247,7 @@ +
@@ -1021,6 +1022,14 @@ TNMH... + + + true + + + Exclude region + + From 881eff2770b1b17cbff1db004255c9bb251c5afd Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 7 Feb 2024 18:11:15 +0000 Subject: [PATCH 12/30] update tool tip; fixes #234 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/238 --- src/gui_qt/data/datawidget/datawidget.py | 106 +++++++++++++---------- src/gui_qt/data/datawidget/properties.py | 6 +- 2 files changed, 65 insertions(+), 47 deletions(-) diff --git a/src/gui_qt/data/datawidget/datawidget.py b/src/gui_qt/data/datawidget/datawidget.py index 45ffc52..e24c10d 100644 --- a/src/gui_qt/data/datawidget/datawidget.py +++ b/src/gui_qt/data/datawidget/datawidget.py @@ -49,11 +49,16 @@ class DataTree(QtWidgets.QTreeWidget): def add_graph(self, idd: str, name: str): item = QtWidgets.QTreeWidgetItem() - item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDropEnabled | QtCore.Qt.ItemIsEditable | - QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable) + item.setFlags( + QtCore.Qt.ItemFlag.ItemIsSelectable | + QtCore.Qt.ItemFlag.ItemIsDropEnabled | + QtCore.Qt.ItemFlag.ItemIsEditable | + QtCore.Qt.ItemFlag.ItemIsEnabled | + QtCore.Qt.ItemFlag.ItemIsUserCheckable + ) item.setText(0, name) - item.setData(0, QtCore.Qt.UserRole, idd) - item.setCheckState(0, QtCore.Qt.Checked) + item.setData(0, QtCore.Qt.ItemDataRole.UserRole, idd) + item.setCheckState(0, QtCore.Qt.CheckState.Checked) self.addTopLevelItem(item) self._checked_graphs.add(idd) @@ -67,14 +72,19 @@ class DataTree(QtWidgets.QTreeWidget): for row in range(self.invisibleRootItem().childCount()): graph = self.invisibleRootItem().child(row) - if graph.data(0, QtCore.Qt.UserRole) == gid: + if graph.data(0, QtCore.Qt.ItemDataRole.UserRole) == gid: for (idd, name, value) in items: item = QtWidgets.QTreeWidgetItem([name]) item.setToolTip(0, f'Value: {value}') - item.setData(0, QtCore.Qt.UserRole, idd) - item.setCheckState(0, QtCore.Qt.Checked) - item.setFlags(QtCore.Qt.ItemIsSelectable | QtCore.Qt.ItemIsDragEnabled | QtCore.Qt.ItemIsEditable | - QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsUserCheckable) + item.setData(0, QtCore.Qt.ItemDataRole.UserRole, idd) + item.setCheckState(0, QtCore.Qt.CheckState.Checked) + item.setFlags( + QtCore.Qt.ItemFlag.ItemIsSelectable | + QtCore.Qt.ItemFlag.ItemIsDragEnabled | + QtCore.Qt.ItemFlag.ItemIsEditable | + QtCore.Qt.ItemFlag.ItemIsEnabled | + QtCore.Qt.ItemFlag.ItemIsUserCheckable + ) graph.addChild(item) self._checked_sets.add(idd) @@ -85,8 +95,8 @@ class DataTree(QtWidgets.QTreeWidget): @QtCore.pyqtSlot(QtWidgets.QTreeWidgetItem) def data_change(self, item: QtWidgets.QTreeWidgetItem, emit: bool = True) -> tuple[set, set]: - idd = item.data(0, QtCore.Qt.UserRole) - is_selected = item.checkState(0) == QtCore.Qt.Checked + idd = item.data(0, QtCore.Qt.ItemDataRole.UserRole) + is_selected = item.checkState(0) == QtCore.Qt.CheckState.Checked to_be_hidden = set() to_be_shown = set() @@ -104,9 +114,9 @@ class DataTree(QtWidgets.QTreeWidget): self.blockSignals(True) for i in range(item.childCount()): child = item.child(i) - child.setCheckState(0, QtCore.Qt.Checked) - to_be_shown.add(child.data(0, QtCore.Qt.UserRole)) - self._checked_sets.add(child.data(0, QtCore.Qt.UserRole)) + child.setCheckState(0, QtCore.Qt.CheckState.Checked) + to_be_shown.add(child.data(0, QtCore.Qt.ItemDataRole.UserRole)) + self._checked_sets.add(child.data(0, QtCore.Qt.ItemDataRole.UserRole)) self.blockSignals(False) # check state change to unchecked @@ -115,10 +125,10 @@ class DataTree(QtWidgets.QTreeWidget): self.blockSignals(True) for i in range(item.childCount()): child = item.child(i) - child.setCheckState(0, QtCore.Qt.Unchecked) - to_be_hidden.add(child.data(0, QtCore.Qt.UserRole)) + child.setCheckState(0, QtCore.Qt.CheckState.Unchecked) + to_be_hidden.add(child.data(0, QtCore.Qt.ItemDataRole.UserRole)) try: - self._checked_sets.remove(child.data(0, QtCore.Qt.UserRole)) + self._checked_sets.remove(child.data(0, QtCore.Qt.ItemDataRole.UserRole)) except KeyError: pass self.blockSignals(False) @@ -153,7 +163,7 @@ class DataTree(QtWidgets.QTreeWidget): @QtCore.pyqtSlot(QtWidgets.QTreeWidgetItem) def new_selection(self, item: QtWidgets.QTreeWidgetItem): if item.parent() is None: - self.management.select_window(item.data(0, QtCore.Qt.UserRole)) + self.management.select_window(item.data(0, QtCore.Qt.ItemDataRole.UserRole)) def dropEvent(self, evt: QtGui.QDropEvent): dropped_index = self.indexAt(evt.pos()) @@ -179,7 +189,7 @@ class DataTree(QtWidgets.QTreeWidget): from_parent.removeChild(it) tobemoved.append(it) - take_from.append(from_parent.data(0, QtCore.Qt.UserRole)) + take_from.append(from_parent.data(0, QtCore.Qt.ItemDataRole.UserRole)) pos = QtCore.QModelIndex(persistent_drop) if self.dropIndicatorPosition() == QtWidgets.QAbstractItemView.BelowItem: @@ -191,8 +201,8 @@ class DataTree(QtWidgets.QTreeWidget): else: to_parent.insertChildren(row, tobemoved) - self.management.move_sets([it.data(0, QtCore.Qt.UserRole) for it in tobemoved], - to_parent.data(0, QtCore.Qt.UserRole), take_from, + self.management.move_sets([it.data(0, QtCore.Qt.ItemDataRole.UserRole) for it in tobemoved], + to_parent.data(0, QtCore.Qt.ItemDataRole.UserRole), take_from, pos=-1 if append else row) self.update_indexes() @@ -207,7 +217,7 @@ class DataTree(QtWidgets.QTreeWidget): while iterator.value(): item = iterator.value() if item is not None: - data = item.data(0, QtCore.Qt.UserRole) + data = item.data(0, QtCore.Qt.ItemDataRole.UserRole) if data == gid_out: from_parent = item @@ -231,7 +241,7 @@ class DataTree(QtWidgets.QTreeWidget): self.blockSignals(False) def sort(self, graph_item: QtWidgets.QTreeWidgetItem, mode: str = 'value'): - graph_id = graph_item.data(0, QtCore.Qt.UserRole) + graph_id = graph_item.data(0, QtCore.Qt.ItemDataRole.UserRole) sets = self.management.get_attributes(graph_id, mode) sets = [el[0] for el in sorted(sets.items(), key=lambda x: x[1])] @@ -243,7 +253,7 @@ class DataTree(QtWidgets.QTreeWidget): for s in sets: for c in children: - if c.data(0, QtCore.Qt.UserRole) == s: + if c.data(0, QtCore.Qt.ItemDataRole.UserRole) == s: graph_item.addChild(c) self.update_indexes() @@ -276,7 +286,7 @@ class DataTree(QtWidgets.QTreeWidget): while iterator.value(): item = iterator.value() if item is not None: - data = item.data(0, QtCore.Qt.UserRole) + data = item.data(0, QtCore.Qt.ItemDataRole.UserRole) if data == sid: if name != item.text(0): item.setText(0, name) @@ -285,7 +295,7 @@ class DataTree(QtWidgets.QTreeWidget): iterator += 1 def keyPressEvent(self, evt: QtGui.QKeyEvent): - if evt.key() == QtCore.Qt.Key_Delete: + if evt.key() == QtCore.Qt.Key.Key_Delete: rm_sets = [] rm_graphs = [] for idx in self.selectedIndexes(): @@ -296,20 +306,20 @@ class DataTree(QtWidgets.QTreeWidget): if item.parent() is None: for c_i in range(item.childCount()): # add sets inside graph to removal - child_data = item.child(c_i).data(0, QtCore.Qt.UserRole) + child_data = item.child(c_i).data(0, QtCore.Qt.ItemDataRole.UserRole) if child_data not in rm_sets: rm_sets.append(child_data) - rm_graphs.append(item.data(0, QtCore.Qt.UserRole)) + rm_graphs.append(item.data(0, QtCore.Qt.ItemDataRole.UserRole)) else: - item_data = item.data(0, QtCore.Qt.UserRole) + item_data = item.data(0, QtCore.Qt.ItemDataRole.UserRole) if item_data not in rm_sets: rm_sets.append(item_data) # self.deleteItem.emit(rm_sets+rm_graphs) self.management.delete_sets(rm_sets+rm_graphs) - elif evt.key() == QtCore.Qt.Key_Space: + elif evt.key() == QtCore.Qt.Key.Key_Space: sets = [] from_parent = [] @@ -329,7 +339,7 @@ class DataTree(QtWidgets.QTreeWidget): for it in sets: if it in from_parent: continue - it.setCheckState(0, QtCore.Qt.Unchecked if it.checkState(0) == QtCore.Qt.Checked else QtCore.Qt.Checked) + it.setCheckState(0, QtCore.Qt.CheckState.Unchecked if it.checkState(0) == QtCore.Qt.CheckState.Checked else QtCore.Qt.CheckState.Checked) s1, s2 = self.data_change(it, emit=False) to_be_hidden |= s2 to_be_shown |= s1 @@ -353,7 +363,7 @@ class DataTree(QtWidgets.QTreeWidget): # find all items that have to be removed while iterator.value(): item = iterator.value() - _id = item.data(0, QtCore.Qt.UserRole) + _id = item.data(0, QtCore.Qt.ItemDataRole.UserRole) if _id in ids: try: item_parent = item.parent() @@ -431,7 +441,7 @@ class DataTree(QtWidgets.QTreeWidget): if i.column() == 0: continue items.append(self.itemFromIndex(i)) - graphs.append(self.itemFromIndex(i).data(0, QtCore.Qt.UserRole)) + graphs.append(self.itemFromIndex(i).data(0, QtCore.Qt.ItemDataRole.UserRole)) if action == del_action: for gid in graphs: @@ -473,12 +483,12 @@ class DataTree(QtWidgets.QTreeWidget): continue else: - graph_id = parent.data(0, QtCore.Qt.UserRole) + graph_id = parent.data(0, QtCore.Qt.ItemDataRole.UserRole) if graph_id not in idx: idx[graph_id] = [] # collect sets in their graph - idx[graph_id].append(item.data(0, QtCore.Qt.UserRole)) - data = self.management[item.data(0, QtCore.Qt.UserRole)] + idx[graph_id].append(item.data(0, QtCore.Qt.ItemDataRole.UserRole)) + data = self.management[item.data(0, QtCore.Qt.ItemDataRole.UserRole)] if data.mode == 'fit': has_fits = True @@ -523,7 +533,7 @@ class DataTree(QtWidgets.QTreeWidget): while iterator.value(): item = iterator.value() if item is not None: - if item.data(0, QtCore.Qt.UserRole) == gid: + if item.data(0, QtCore.Qt.ItemDataRole.UserRole) == gid: item.setBackground(0, QtGui.QBrush(QtGui.QColor('gray'))) else: item.setBackground(0, QtGui.QBrush()) @@ -536,10 +546,10 @@ class DataTree(QtWidgets.QTreeWidget): while iterator.value(): item = iterator.value() if item is not None: - if item.data(0, QtCore.Qt.UserRole) in sets: - item.setCheckState(0, QtCore.Qt.Unchecked) + if item.data(0, QtCore.Qt.ItemDataRole.UserRole) in sets: + item.setCheckState(0, QtCore.Qt.CheckState.Unchecked) else: - self._checked_sets.add(item.data(0, QtCore.Qt.UserRole)) + self._checked_sets.add(item.data(0, QtCore.Qt.ItemDataRole.UserRole)) iterator += 1 self.blockSignals(False) @@ -594,7 +604,7 @@ class DataWidget(QtWidgets.QWidget, Ui_DataWidget): sid = [] for i in self.tree.selectedIndexes(): if i.column() == 0: - sid.append(i.data(role=QtCore.Qt.UserRole)) + sid.append(i.data(role=QtCore.Qt.ItemDataRole.UserRole)) self.startShowProperty.emit(sid) @@ -603,15 +613,23 @@ class DataWidget(QtWidgets.QWidget, Ui_DataWidget): self.proptable.populate(props) def change_property(self, key1, key2, value): - ids = [item.data(0, QtCore.Qt.UserRole) for item in self.tree.selectedItems()] if key2 == 'Value': try: value = float(value) except ValueError: - QtWidgets.QMessageBox.warning(self, 'Invalid entry', - 'Value %r is not a valid number for `value`.' % value) + QtWidgets.QMessageBox.warning( + self, + 'Invalid entry', + f'Value {value!r} is not a valid number for `value`.') return + ids = [] + for item in self.tree.selectedItems(): + ids.append(item.data(0, QtCore.Qt.ItemDataRole.UserRole)) + item.setToolTip(0, str(value)) + else: + ids = [item.data(0, QtCore.Qt.ItemDataRole.UserRole) for item in self.tree.selectedItems()] + self.propertyChanged.emit(ids, key1, key2, value) def uncheck_sets(self, sets: list[str]): diff --git a/src/gui_qt/data/datawidget/properties.py b/src/gui_qt/data/datawidget/properties.py index dd07087..21e3e2d 100644 --- a/src/gui_qt/data/datawidget/properties.py +++ b/src/gui_qt/data/datawidget/properties.py @@ -41,7 +41,7 @@ class PropWidget(QtWidgets.QWidget): idx = table.indexFromItem(item) self.propertyChanged.emit(self.tab.tabText(tab_idx), table.item(idx.row(), idx.column()-1).text(), - item.data(QtCore.Qt.DisplayRole)) + item.data(QtCore.Qt.ItemDataRole.DisplayRole)) @QtCore.pyqtSlot(int) def tab_change(self, idx: int): @@ -66,10 +66,10 @@ class PropTable(QtWidgets.QTableWidget): self.blockSignals(True) for k, v in prop.items(): value_item = QtWidgets.QTableWidgetItem('') - value_item.setData(QtCore.Qt.DisplayRole, v) + value_item.setData(QtCore.Qt.ItemDataRoleDisplayRole, v) key_item = QtWidgets.QTableWidgetItem(k) - key_item.setFlags(QtCore.Qt.NoItemFlags) + key_item.setFlags(QtCore.Qt.ItemDataRole.NoItemFlags) key_item.setForeground(QtGui.QBrush(QtGui.QColor(0, 0, 0))) self.setRowCount(self.rowCount()+1) From 24640d374ea418c4e5d2c898cb93b52ec9f400f5 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 7 Feb 2024 18:58:18 +0000 Subject: [PATCH 13/30] handle graph export with empty data (#239); closes #233 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/239 --- src/gui_qt/data/valueeditwidget.py | 43 ++++++++++++++++-------------- src/gui_qt/graphs/graphwindow.py | 27 ++++++++++++------- src/gui_qt/lib/pg_objects.py | 5 +++- 3 files changed, 44 insertions(+), 31 deletions(-) diff --git a/src/gui_qt/data/valueeditwidget.py b/src/gui_qt/data/valueeditwidget.py index c85f1d1..ae95a5f 100644 --- a/src/gui_qt/data/valueeditwidget.py +++ b/src/gui_qt/data/valueeditwidget.py @@ -3,10 +3,10 @@ from __future__ import annotations from typing import Any from numpy import ndarray, iscomplexobj, asarray -from pyqtgraph import PlotDataItem from ..Qt import QtGui, QtCore, QtWidgets from .._py.valueeditor import Ui_MaskDialog +from ..lib.pg_objects import PlotItem class ValueEditWidget(QtWidgets.QWidget, Ui_MaskDialog): @@ -35,13 +35,13 @@ class ValueEditWidget(QtWidgets.QWidget, Ui_MaskDialog): self.tableView.setModel(self.model) self.tableView.setSelectionModel(self.selection_model) - self.tableView.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.tableView.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.tableView.customContextMenuRequested.connect(self.ctx) - self.selection_real = PlotDataItem(x=[], y=[], symbolSize=25, symbol='x', - pen=None, symbolPen='#c9308e', symbolBrush='#c9308e') - self.selection_imag = PlotDataItem(x=[], y=[], symbolSize=25, symbol='+', - pen=None, symbolPen='#dcdcdc', symbolBrush='#dcdcdc') + self.selection_real = PlotItem(x=[], y=[], symbolSize=25, symbol='x', + pen=None, symbolPen='#c9308e', symbolBrush='#c9308e') + self.selection_imag = PlotItem(x=[], y=[], symbolSize=25, symbol='+', + pen=None, symbolPen='#dcdcdc', symbolBrush='#dcdcdc') def __call__(self, items: dict): self.items = items @@ -133,7 +133,7 @@ class ValueEditWidget(QtWidgets.QWidget, Ui_MaskDialog): def keyPressEvent(self, evt): if evt.matches(QtGui.QKeySequence.Copy): self.copy_selection() - elif evt.key() == QtCore.Qt.Key_Delete: + elif evt.key() == QtCore.Qt.Key.Key_Delete: self.delete_item() else: super().keyPressEvent(evt) @@ -229,7 +229,7 @@ class ValueModel(QtCore.QAbstractTableModel): """ itemChanged = QtCore.pyqtSignal(int, int, str) load_number = 20 - maskRole = QtCore.Qt.UserRole+321 + maskRole = QtCore.Qt.ItemDataRole.UserRole+321 def __init__(self, parent=None): super().__init__(parent=parent) @@ -240,7 +240,7 @@ class ValueModel(QtCore.QAbstractTableModel): self.mask = None self.headers = ['x', 'y', '\u0394y'] for i, hd in enumerate(self.headers): - self.setHeaderData(i, QtCore.Qt.Horizontal, hd) + self.setHeaderData(i, QtCore.Qt.Orientation.Horizontal, hd) def rowCount(self, *args, **kwargs) -> int: return self.total_rows @@ -258,25 +258,28 @@ class ValueModel(QtCore.QAbstractTableModel): self.mask = mask.tolist() self.endResetModel() - self.dataChanged.emit(self.index(0, 0), self.index(0, 1), [QtCore.Qt.DisplayRole]) + self.dataChanged.emit( + self.index(0, 0), + self.index(0, 1), [QtCore.Qt.ItemDataRole.DisplayRole] + ) - def data(self, idx: QtCore.QModelIndex, role=QtCore.Qt.DisplayRole) -> Any: + def data(self, idx: QtCore.QModelIndex, role=QtCore.Qt.ItemDataRole.DisplayRole) -> Any: if not idx.isValid(): return row = idx.row() - if role in [QtCore.Qt.DisplayRole, QtCore.Qt.EditRole]: + if role in [QtCore.Qt.ItemDataRole.DisplayRole, QtCore.Qt.ItemDataRole.EditRole]: val = self._data[row][idx.column()] return self.as_string(val) - elif role == QtCore.Qt.BackgroundRole: + elif role == QtCore.Qt.ItemDataRole.BackgroundRole: pal = QtGui.QGuiApplication.palette() if not self.mask[row]: return pal.color(QtGui.QPalette.Disabled, QtGui.QPalette.Base) else: return pal.color(QtGui.QPalette.Base) - elif role == QtCore.Qt.ForegroundRole: + elif role == QtCore.Qt.ItemDataRole.ForegroundRole: pal = QtGui.QGuiApplication.palette() if not self.mask[row]: return pal.color(QtGui.QPalette.Disabled, QtGui.QPalette.Text) @@ -289,7 +292,7 @@ class ValueModel(QtCore.QAbstractTableModel): else: return - def setData(self, idx: QtCore.QModelIndex, value: str | bool, role=QtCore.Qt.DisplayRole) -> Any: + def setData(self, idx: QtCore.QModelIndex, value: str | bool, role=QtCore.Qt.ItemDataRole.DisplayRole) -> Any: col, row = idx.column(), idx.row() if role == ValueModel.maskRole: @@ -299,7 +302,7 @@ class ValueModel(QtCore.QAbstractTableModel): return True if value: - if role == QtCore.Qt.EditRole: + if role == QtCore.Qt.ItemDataRole.EditRole: if value == self.as_string(self._data[row][col]): return True @@ -322,9 +325,9 @@ class ValueModel(QtCore.QAbstractTableModel): else: return False - def headerData(self, section: int, orientation, role=QtCore.Qt.DisplayRole) -> Any: - if role == QtCore.Qt.DisplayRole: - if orientation == QtCore.Qt.Horizontal: + def headerData(self, section: int, orientation, role=QtCore.Qt.ItemDataRole.DisplayRole) -> Any: + if role == QtCore.Qt.ItemDataRole.DisplayRole: + if orientation == QtCore.Qt.Orientation.Horizontal: return self.headers[section] else: return str(section+1) @@ -346,7 +349,7 @@ class ValueModel(QtCore.QAbstractTableModel): self.endInsertRows() def flags(self, idx: QtCore.QModelIndex) -> QtCore.Qt.ItemFlag: - return QtCore.QAbstractTableModel.flags(self, idx) | QtCore.Qt.ItemIsEditable + return QtCore.QAbstractTableModel.flags(self, idx) | QtCore.Qt.ItemFlag.ItemIsEditable def removeRows(self, pos: int, rows: int, parent=None, *args, **kwargs) -> bool: self.beginRemoveRows(parent, pos, pos+rows-1) diff --git a/src/gui_qt/graphs/graphwindow.py b/src/gui_qt/graphs/graphwindow.py index 9369bf3..405e520 100644 --- a/src/gui_qt/graphs/graphwindow.py +++ b/src/gui_qt/graphs/graphwindow.py @@ -670,11 +670,14 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): else: if os.path.exists(outfile): - if QtWidgets.QMessageBox.warning(self, 'Export graphic', - f'{os.path.split(outfile)[1]} already exists.\n' - f'Do you REALLY want to replace it?', - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.No) == QtWidgets.QMessageBox.No: + if QtWidgets.QMessageBox.warning( + self, + 'Export graphic', + f'{os.path.split(outfile)[1]} already exists.\n' + f'Do you REALLY want to replace it?', + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.No + ) == QtWidgets.QMessageBox.No: return bg_color = self._bgcolor @@ -716,16 +719,20 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): logger.exception(f'{item} could not exported because {e.args}') continue - if len(item) == 2: - # plot can show errorbars - item_dic['yerr'] = item[1].opts['topData'] - if item_dic: + if len(item) == 2: + # plot can show errorbars + if len(item_dic['x']): + item_dic['yerr'] = item[1].opts['topData'] + else: + item_dic['yerr'] = [] dic['items'].append(item_dic) for item in self._external_items: try: - dic['items'].append(item.get_data_opts()) + item_dic = item.get_data_opts() + if item_dic: + dic['items'].append(item_dic) except Exception as e: logger.exception(f'{item} could not be exported because {e.args}') continue diff --git a/src/gui_qt/lib/pg_objects.py b/src/gui_qt/lib/pg_objects.py index 923ca88..fa678e1 100644 --- a/src/gui_qt/lib/pg_objects.py +++ b/src/gui_qt/lib/pg_objects.py @@ -183,6 +183,8 @@ class PlotItem(PlotDataItem): brush = self.opts['symbolBrush'] if isinstance(brush, tuple): self.opts['symbolcolor'] = brush + elif isinstance(brush, str): + self.opts['symbolcolor'] = int(f'0x{brush[1:3]}', 16), int(f'0x{brush[3:5]}', 16), int(f'0x{brush[5:7]}', 16) else: c = brush.color() self.opts['symbolcolor'] = c.red(), c.green(), c.blue() @@ -340,7 +342,8 @@ class PlotItem(PlotDataItem): opts = self.opts item_dic = { - 'x': x, 'y': y, + 'x': x, + 'y': y, 'name': opts.get('name', ''), 'symbolsize': opts['symbolSize'], } From 8d3ab75c979265bd0aba2f3ac2b7e31b89eb988c Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sat, 10 Feb 2024 16:46:15 +0000 Subject: [PATCH 14/30] bugfix (#241) Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/241 --- src/gui_qt/data/datawidget/properties.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui_qt/data/datawidget/properties.py b/src/gui_qt/data/datawidget/properties.py index 21e3e2d..cb4266a 100644 --- a/src/gui_qt/data/datawidget/properties.py +++ b/src/gui_qt/data/datawidget/properties.py @@ -66,10 +66,10 @@ class PropTable(QtWidgets.QTableWidget): self.blockSignals(True) for k, v in prop.items(): value_item = QtWidgets.QTableWidgetItem('') - value_item.setData(QtCore.Qt.ItemDataRoleDisplayRole, v) + value_item.setData(QtCore.Qt.ItemDataRole.DisplayRole, v) key_item = QtWidgets.QTableWidgetItem(k) - key_item.setFlags(QtCore.Qt.ItemDataRole.NoItemFlags) + key_item.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) key_item.setForeground(QtGui.QBrush(QtGui.QColor(0, 0, 0))) self.setRowCount(self.rowCount()+1) From 80d9c7098cb5cbe63dc8271fdb4c8955b7b294a2 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sun, 11 Feb 2024 17:40:50 +0000 Subject: [PATCH 15/30] 207-noncomplex-fits (#244) Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/244 closes #207 --- src/gui_qt/main/management.py | 22 +++++- src/nmreval/fit/data.py | 5 +- src/nmreval/fit/minimizer.py | 98 ++++++++++++----------- src/nmreval/fit/model.py | 2 +- src/nmreval/fit/result.py | 142 ++++++++++++++++++---------------- 5 files changed, 152 insertions(+), 117 deletions(-) diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index ab07037..4b959ee 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -503,11 +503,25 @@ class UpperManagement(QtCore.QObject): we = we_option if m_complex is None or m_complex == 1: + # model is not complex: m_complex = None + # model is complex, fit real part: m_complex = 1 _y = data_i.y.real - elif m_complex == 2 and np.iscomplexobj(data_i.y): - _y = data_i.y.imag + data_complex = 1 + elif m_complex == 2: + # model is complex, fit imag part: m_complex = 2 + if np.iscomplexobj(data_i.y): + # data is complex, use imag part + _y = data_i.y.imag + data_complex = 2 + else: + # data is real + _y = data_i.y + data_complex = 1 else: + # model is complex, fit complex: m_complex = 0 + # use data as given (complex or not) _y = data_i.y + data_complex = 0 _x = data_i.x @@ -524,9 +538,9 @@ class UpperManagement(QtCore.QObject): try: if isinstance(we, str): - d = fit_d.Data(_x[inside], _y[inside], we=we, idx=set_id) + d = fit_d.Data(_x[inside], _y[inside], we=we, idx=set_id, complex_type=data_complex) else: - d = fit_d.Data(_x[inside], _y[inside], we=we[inside], idx=set_id) + d = fit_d.Data(_x[inside], _y[inside], we=we[inside], idx=set_id, complex_type=data_complex) except Exception as e: raise Exception(f'Setting data failed for {set_id}') diff --git a/src/nmreval/fit/data.py b/src/nmreval/fit/data.py index f6f2cc5..b6b94ff 100644 --- a/src/nmreval/fit/data.py +++ b/src/nmreval/fit/data.py @@ -6,8 +6,8 @@ from .model import Model from .parameter import Parameters, Parameter -class Data(object): - def __init__(self, x, y, we=None, idx=None): +class Data: + def __init__(self, x, y, we=None, idx=None, complex_type: int = 0): self.x = np.asarray(x) self.y = np.asarray(y) if self.y.shape[0] != self.x.shape[0]: @@ -20,6 +20,7 @@ class Data(object): self.parameter = Parameters() self.para_keys: list = [] self.fun_kwargs = {} + self.complex_type = complex_type def __len__(self): return self.y.shape[0] diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index 27e7492..78f4557 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -361,7 +361,7 @@ class FitRoutine(object): with np.errstate(all='ignore'): res = optimize.least_squares(cost, p0, bounds=(lb, ub), max_nfev=500 * len(p0)) - err, corr, partial_corr = self._calc_error(res.jac, np.sum(res.fun**2), *res.jac.shape) + err, corr, partial_corr = _calc_error(res.jac, np.sum(res.fun**2), *res.jac.shape) self.make_results(data, res.x, var, data.para_keys, res.jac.shape, err=err, corr=corr, partial_corr=partial_corr) @@ -375,7 +375,7 @@ class FitRoutine(object): with np.errstate(all='ignore'): res = optimize.least_squares(cost, p0, bounds=(lb, ub), max_nfev=500 * len(p0)) - err, corr, partial_corr = self._calc_error(res.jac, np.sum(res.fun**2), *res.jac.shape) + err, corr, partial_corr = _calc_error(res.jac, np.sum(res.fun**2), *res.jac.shape) for v, var_pars_k in zip(data, data_pars): self.make_results(v, res.x, var, var_pars_k, res.jac.shape, err=err, corr=corr, partial_corr=partial_corr) @@ -458,9 +458,17 @@ class FitRoutine(object): self.make_results(v, res.beta, var, var_pars_k, (sum(len(d) for d in data), len(p0)), err=res.sd_beta, corr=corr, partial_corr=partial_corr) - def make_results(self, data, p, var_pars, used_pars, shape, - err=None, corr=None, partial_corr=None): - + def make_results( + self, + data: Data, + p: list[float], + var_pars: list[str], + used_pars: list[str], + shape: tuple[int, int], + err: list[float] = None, + corr: np.ndarray = None, + partial_corr: np.ndarray = None, + ): if err is None: err = [0] * len(p) @@ -498,52 +506,54 @@ class FitRoutine(object): model = data.get_model() self.result[idx] = FitResultCreator.make_with_model( - model, - data.x, - data.y, - actual_parameters, - data.fun_kwargs, - data.we_string, - data.idx, - *shape, + model=model, + x_orig=data.x, + y_orig=data.y, + p=actual_parameters, + fun_kwargs=data.fun_kwargs, + we=data.we_string, + idx=data.idx, + nobs=shape[0], + nvar=shape[1], corr=actual_corr, pcorr=actual_pcorr, + data_mode=data.complex_type, ) return self.result - @staticmethod - def _calc_error(jac, chi, nobs, nvars): - # copy of scipy.curve_fit to calculate covariance - # noinspection PyTupleAssignmentBalance - try: - _, s, vt = la.svd(jac, full_matrices=False) - except ValueError as e: - # this may be issue #39: On entry to DGESSD parameter had an illegal value - # catch this exception and ignore error calculation - logger.error(f'Error calculation failed with {e.args}') - pcov = None - else: - threshold = EPS * max(jac.shape) * s[0] - s = s[s > threshold] - vt = vt[:s.size] - pcov = np.dot(vt.T / s**2, vt) * chi / (nobs - nvars) - if pcov is None: - _err = np.zeros(nvars) - corr = np.zeros((nvars, nvars)) - else: - _err = np.sqrt(np.diag(pcov)) - corr = pcov / (_err[:, None] * _err[None, :]) +def _calc_error(jac, chi, nobs, nvars): + # copy of scipy.curve_fit to calculate covariance + # noinspection PyTupleAssignmentBalance + try: + _, s, vt = la.svd(jac, full_matrices=False) + except ValueError as e: + # this may be issue #39: On entry to DGESSD parameter had an illegal value + # catch this exception and ignore error calculation + logger.error(f'Error calculation failed with {e.args}') + pcov = None + else: + threshold = EPS * max(jac.shape) * s[0] + s = s[s > threshold] + vt = vt[:s.size] + pcov = np.dot(vt.T / s**2, vt) * chi / (nobs - nvars) - corr = corr.astype(np.float64) - try: - corr_inv = np.linalg.inv(corr) - corr_inv_diag = np.diag(np.sqrt(1 / np.diag(corr_inv))) - partial_corr = -1. * np.dot(np.dot(corr_inv_diag, corr_inv), corr_inv_diag) # Partial correlation matrix - partial_corr[np.diag_indices_from(partial_corr)] = 1. - except np.linalg.LinAlgError: - partial_corr = corr + if pcov is None: + _err = np.zeros(nvars) + corr = np.zeros((nvars, nvars)) + else: + _err = np.sqrt(np.diag(pcov)) + corr = pcov / (_err[:, None] * _err[None, :]) - return _err, corr, partial_corr + corr = corr.astype(np.float64) + try: + corr_inv = np.linalg.inv(corr) + corr_inv_diag = np.diag(np.sqrt(1 / np.diag(corr_inv))) + partial_corr = -1. * np.dot(np.dot(corr_inv_diag, corr_inv), corr_inv_diag) # Partial correlation matrix + partial_corr[np.diag_indices_from(partial_corr)] = 1. + except np.linalg.LinAlgError: + partial_corr = corr + + return _err, corr, partial_corr diff --git a/src/nmreval/fit/model.py b/src/nmreval/fit/model.py index c80a2a3..46aacee 100644 --- a/src/nmreval/fit/model.py +++ b/src/nmreval/fit/model.py @@ -9,7 +9,7 @@ from ._meta import MultiModel from .parameter import Parameters, Parameter -class Model(object): +class Model: def __init__(self, model, *args, **kwargs): self.idx = kwargs.pop('idx', None) diff --git a/src/nmreval/fit/result.py b/src/nmreval/fit/result.py index 87fa94f..d77fd88 100644 --- a/src/nmreval/fit/result.py +++ b/src/nmreval/fit/result.py @@ -11,6 +11,7 @@ from scipy.stats import f as fdist from scipy.interpolate import interp1d from ._meta import MultiModel +from .model import Model from .parameter import Parameter from ..data.points import Points from ..data.signals import Signal @@ -36,17 +37,30 @@ class FitResultCreator: else: resid = kwargs['y'] - y_orig - stats = FitResultCreator.calc_statistics(resid, _y) + stats = 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) + return FitResult( + x=kwargs['x'], + y=kwargs['y'], + x_data=x_orig, + y_data=y_orig, + params=params, + fun_kwargs=dict(kwargs['choice']), + resid=resid, + nobs=0, + nvar=0, + we='', + name=kwargs['name'], + stats=stats, + idx=idx, + ) @staticmethod def make_with_model( model: 'Model', x_orig: np.ndarray, y_orig: np.ndarray, - p: 'Parameters', + p: list, fun_kwargs: dict, we: str, idx: str | None, @@ -54,6 +68,7 @@ class FitResultCreator: nvar: int, corr: np.ndarray, pcorr: np.ndarray, + data_mode: int, ) -> FitResult: if np.all(x_orig > 0) and (np.max(x_orig) > 100 * np.min(x_orig)): islog = True @@ -83,17 +98,11 @@ class FitResultCreator: actual_mode = fun_kwargs['complex_mode'] fun_kwargs['complex_mode'] = 0 - _y = model.func(p_final, _x, **fun_kwargs) + _y = check_complex(model.func(p_final, _x, **fun_kwargs), actual_mode, data_mode) - if not actual_mode < 0: - if actual_mode == 1: - _y.imag = 0 - elif actual_mode == 2: - _y.real = 0 + fun_kwargs['complex_mode'] = actual_mode - fun_kwargs['complex_mode'] = actual_mode - - stats = FitResultCreator.calc_statistics(_y, resid, nobs, nvar) + stats = calc_statistics(_y, resid, nobs, nvar) varied = [p.var for p in parameters.values()] if corr is None: @@ -134,38 +143,9 @@ class FitResultCreator: pcorr=partial_correlation, islog=islog, func=model, + data_complex=data_mode, ) - @staticmethod - def calc_statistics(y, residual, nobs=None, nvar=None): - chi = (residual**2).sum() - try: - r = 1 - chi/((y-np.mean(y))**2).sum() - except RuntimeWarning: - r = -9999 - - if nobs is None: - nobs = 1 - - if nvar is None: - nvar = 0 - - dof = nobs - nvar - loglikehood = nobs * np.log(chi / nobs) - - stats = { - 'chi^2': chi, - 'R^2': r, - 'AIC': loglikehood + 2 * nvar, - 'BIC': loglikehood + np.log(nobs) * nvar, - 'adj. R^2': 1 - (nobs-1) / (dof+1e-13) * (1-r), - 'red. chi^2': chi / (dof + 1e-13), - } - - stats['AICc'] = stats['AIC'] + 2*(nvar+1)*nvar / (dof - 1 + 1e-13) - - return stats - class FitResult(Points): @@ -188,7 +168,8 @@ class FitResult(Points): pcorr: np.ndarray = None, islog: bool = False, func=None, - **kwargs + data_complex: int = 1, + **kwargs, ): self.parameter, name = self._prepare_names(params, name) @@ -210,6 +191,7 @@ class FitResult(Points): self.y_data = y_data self._model_name = name self._func = func + self._data_complex = data_complex @staticmethod def _prepare_names(parameter: dict, modelname: str): @@ -418,20 +400,9 @@ class FitResult(Points): if self.func is None: raise ValueError('no fit function available to calculate new y values') - actual_mode = -1 - if 'complex_mode' in self.fun_kwargs: - actual_mode = self.fun_kwargs['complex_mode'] - self.fun_kwargs['complex_mode'] = 0 - new_fit = self.copy() y_values = self.func.func(self.p_final, x_values, **self.fun_kwargs) - if not actual_mode < 0: - if actual_mode == 1: - y_values.imag = 0 - elif actual_mode == 2: - y_values.real = 0 - - self.fun_kwargs['complex_mode'] = actual_mode + y_values = check_complex(y_values, self.fun_kwargs.get('complex_mode', -1), self._data_complex) new_fit.set_data(x_values, y_values, y_err=0.0) @@ -442,20 +413,13 @@ class FitResult(Points): raise ValueError('no fit function available to calculate new y values') part_functions = [] - actual_mode = -1 - if 'complex_mode' in self.fun_kwargs: - actual_mode = self.fun_kwargs['complex_mode'] - self.fun_kwargs['complex_mode'] = 0 + actual_mode = self.fun_kwargs.get('complex_mode', -1) for sub_name, sub_y in zip(self.func.sub_name(), self.func.sub(self.p_final, x_values, **self.fun_kwargs)): - if not actual_mode < 0: - if actual_mode == 1: - sub_y.imag = 0 - elif actual_mode == 2: - sub_y.real = 0 + sub_y = check_complex(sub_y, actual_mode, self._data_complex) + 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)) @@ -463,3 +427,49 @@ class FitResult(Points): self.fun_kwargs['complex_mode'] = actual_mode return part_functions + + +def check_complex(y, model_complex, data_complex): + if not np.iscomplexobj(y): + return y + + if model_complex == 1: + y.imag = 0 + if data_complex == 1: + y = y.real + elif model_complex == 2: + y.real = 0 + if data_complex == 1: + y = y.imag + + return y + + +def calc_statistics(y, residual, nobs=None, nvar=None): + chi = (residual**2).sum() + try: + r = 1 - chi/((y-np.mean(y))**2).sum() + except RuntimeWarning: + r = -9999 + + if nobs is None: + nobs = 1 + + if nvar is None: + nvar = 0 + + dof = nobs - nvar + loglikehood = nobs * np.log(chi / nobs) + + stats = { + 'chi^2': chi, + 'R^2': r, + 'AIC': loglikehood + 2 * nvar, + 'BIC': loglikehood + np.log(nobs) * nvar, + 'adj. R^2': 1 - (nobs-1) / (dof+1e-13) * (1-r), + 'red. chi^2': chi / (dof + 1e-13), + } + + stats['AICc'] = stats['AIC'] + 2*(nvar+1)*nvar / (dof - 1 + 1e-13) + + return stats From ffba4900a18b15495f2c416d5342bba6c14ddabb Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 13 Feb 2024 16:17:08 +0000 Subject: [PATCH 16/30] remove complex kwarg when necessary (#246) closes #245 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/246 --- src/nmreval/fit/result.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/nmreval/fit/result.py b/src/nmreval/fit/result.py index d77fd88..bd954b4 100644 --- a/src/nmreval/fit/result.py +++ b/src/nmreval/fit/result.py @@ -401,7 +401,10 @@ class FitResult(Points): raise ValueError('no fit function available to calculate new y values') new_fit = self.copy() - y_values = self.func.func(self.p_final, x_values, **self.fun_kwargs) + fun_kwargs = {k: v for k, v in self.fun_kwargs.items()} + if self.fun_kwargs.get('complex_mode', -1) == -1: + fun_kwargs.pop('complex_mode', None) + y_values = self.func.func(self.p_final, x_values, **fun_kwargs) y_values = check_complex(y_values, self.fun_kwargs.get('complex_mode', -1), self._data_complex) new_fit.set_data(x_values, y_values, y_err=0.0) @@ -414,6 +417,9 @@ class FitResult(Points): part_functions = [] actual_mode = self.fun_kwargs.get('complex_mode', -1) + fun_kwargs = {k: v for k, v in self.fun_kwargs.items()} + if self.fun_kwargs.get('complex_mode', -1) == -1: + fun_kwargs.pop('complex_mode', None) for sub_name, sub_y in zip(self.func.sub_name(), self.func.sub(self.p_final, x_values, **self.fun_kwargs)): sub_y = check_complex(sub_y, actual_mode, self._data_complex) From 04d384363ae59d2e951d35c937c9e65812a7093f Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 20 Feb 2024 16:19:23 +0000 Subject: [PATCH 17/30] 249-asciireader (#250) Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/250 --- src/gui_qt/io/asciireader.py | 6 +++--- src/nmreval/io/asciireader.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/gui_qt/io/asciireader.py b/src/gui_qt/io/asciireader.py index a4c9524..2520844 100644 --- a/src/gui_qt/io/asciireader.py +++ b/src/gui_qt/io/asciireader.py @@ -38,7 +38,7 @@ class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader): self.changestaggeredrange(0) self.ascii_table.contextMenuEvent = self.ctx_table - self.ascii_table.horizontalHeader().setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + self.ascii_table.horizontalHeader().setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) self.ascii_table.horizontalHeader().customContextMenuRequested.connect(self.ctx_table) self.skip = False @@ -65,7 +65,7 @@ class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader): self.set_column_names(1) self.skippy_checkbox.blockSignals(True) - self.skippy_checkbox.setCheckState(QtCore.Qt.Unchecked) + self.skippy_checkbox.setCheckState(QtCore.Qt.CheckState.Unchecked) self.skippy_checkbox.blockSignals(False) return self @@ -232,7 +232,7 @@ class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader): @QtCore.pyqtSlot(int, name='on_buttonGroup_buttonClicked') def show_error(self, val: int): - self.deltay_lineEdit.setEnabled(val == -3) + self.deltay_lineEdit.setEnabled(val == -2) @QtCore.pyqtSlot(int, name='on_skippy_checkbox_stateChanged') def skip_next_dial(self, _: int): diff --git a/src/nmreval/io/asciireader.py b/src/nmreval/io/asciireader.py index 05f84a5..6a31318 100644 --- a/src/nmreval/io/asciireader.py +++ b/src/nmreval/io/asciireader.py @@ -49,7 +49,7 @@ class AsciiReader: with self.fname.open('r') as f: for i, line in enumerate(islice(f, len(self.header)+len(self.lines), num_lines)): line = line.strip('\n\t\r, ') - line = re.sub(r'[\t ;,] *', ';', line) + line = re.sub(r'[\t ;,]+', ';', line) line = line.split(';') try: From 24f77f753c9aa0f1bc140635cb12e1f990b992e5 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 27 Feb 2024 14:20:08 +0000 Subject: [PATCH 18/30] 242-uncaught-exception (#252) close issue #242 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/252 --- src/gui_qt/lib/logger.py | 27 +++++++++++++++++++++++++++ src/gui_qt/lib/utils.py | 2 +- src/gui_qt/main/console.py | 3 +++ src/gui_qt/main/mainwindow.py | 6 ++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 src/gui_qt/main/console.py diff --git a/src/gui_qt/lib/logger.py b/src/gui_qt/lib/logger.py index 0570d84..e1cb93c 100644 --- a/src/gui_qt/lib/logger.py +++ b/src/gui_qt/lib/logger.py @@ -1,5 +1,8 @@ +import logging from pathlib import Path +from PyQt5 import QtWidgets + from .codeeditor import _make_textformats from ..Qt import QtWidgets, QtCore, QtGui from nmreval.configs import config_paths @@ -113,3 +116,27 @@ class QLog(QtWidgets.QDialog): for lines in text[-100:]: self.plainTextEdit.appendPlainText(lines[:-1]) + + +class ConsoleDock(QtWidgets.QDockWidget): + def __init__(self, parent=None): + super().__init__(parent=parent) + + self.code = QtWidgets.QPlainTextEdit(parent) + self.code.highlight = LogHighlighter(self.code.document()) + self.code.setReadOnly(True) + self.code.setMaximumBlockCount(50) + self.setWidget(self.code) + + +class QTextHandler(logging.Handler): + def __init__(self, parent): + super().__init__() + + self.console = ConsoleDock(parent) + self.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + + def emit(self, record): + msg = self.format(record) + self.console.code.appendPlainText(msg) + self.console.show() diff --git a/src/gui_qt/lib/utils.py b/src/gui_qt/lib/utils.py index 2f06820..5d87d1c 100644 --- a/src/gui_qt/lib/utils.py +++ b/src/gui_qt/lib/utils.py @@ -10,7 +10,7 @@ from ..Qt import QtGui, QtWidgets, QtCore @contextmanager def busy_cursor(): try: - cursor = QtGui.QCursor(QtCore.Qt.ForbiddenCursor) + cursor = QtGui.QCursor(QtCore.Qt.CursorShape.ForbiddenCursor) QtWidgets.QApplication.setOverrideCursor(cursor) yield diff --git a/src/gui_qt/main/console.py b/src/gui_qt/main/console.py new file mode 100644 index 0000000..b28b04f --- /dev/null +++ b/src/gui_qt/main/console.py @@ -0,0 +1,3 @@ + + + diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index d91b3ac..7f637d4 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -12,6 +12,7 @@ from nmreval.lib.logger import logger from nmreval.io.sessionwriter import NMRWriter from .management import UpperManagement +from ..lib.logger import ConsoleDock, QTextHandler from ..Qt import QtGui, QtPrintSupport from ..data.shift_graphs import QShift from ..data.signaledit import QPreviewDialog, QBaselineDialog @@ -62,6 +63,11 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.eval = None self.editor = None + self.logtext = QTextHandler(self) + logger.addHandler(self.logtext) + self.addDockWidget(QtCore.Qt.DockWidgetArea.BottomDockWidgetArea, self.logtext.console) + self.logtext.console.hide() + self.movedialog = QMover(self) self.current_graph_widget = None From 2f9cb761cf2262b0de2305b543e6b976b224708c Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 27 Feb 2024 15:36:14 +0000 Subject: [PATCH 19/30] use tree colors in fit result for sub-funcs (#253) closes #201 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/253 --- src/gui_qt/fit/result.py | 51 +++++++++++++++++++++-------------- src/gui_qt/main/mainwindow.py | 8 +++--- src/gui_qt/main/management.py | 20 ++++++++++---- 3 files changed, 50 insertions(+), 29 deletions(-) diff --git a/src/gui_qt/fit/result.py b/src/gui_qt/fit/result.py index 4706cbc..3676175 100644 --- a/src/gui_qt/fit/result.py +++ b/src/gui_qt/fit/result.py @@ -2,7 +2,7 @@ from __future__ import annotations from math import isnan -from pyqtgraph import mkBrush, mkPen +from pyqtgraph import mkBrush, mkPen, mkColor from numpy import abs as np_abs, isfinite as np_isfinite from nmreval.utils.text import convert @@ -18,7 +18,7 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): closed = QtCore.pyqtSignal(dict, list, str, bool, bool, list) redoFit = QtCore.pyqtSignal(dict) - def __init__(self, results: list, management, parent=None): + def __init__(self, results: list, sub_colors: dict, management, parent=None): super().__init__(parent=parent) self.setupUi(self) @@ -37,6 +37,7 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): self._results = {} self.graph_opts = {} self.last_idx = None + self.func_colors = sub_colors self.fit_plot = self.graphicsView.addPlot(row=1, col=0, title='Fit') self.resid_plot = self.graphicsView.addPlot(row=0, col=0, title='Residual') @@ -48,21 +49,29 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): self.graphicsView.ci.layout.setRowStretchFactor(0, 1) self.graphicsView.ci.layout.setRowStretchFactor(1, 2) - self.resid_graph = PlotItem(x=[], y=[], - symbol='o', symbolPen=None, symbolBrush=mkBrush(color=(31, 119, 180)), - pen=None) - self.resid_graph_imag = PlotItem(x=[], y=[], - symbol='s', symbolPen=None, symbolBrush=mkBrush(color=(255, 127, 14)), - pen=None) + self.resid_graph = PlotItem( + x=[], y=[], + symbol='o', symbolPen=None, symbolBrush=mkBrush(color=(31, 119, 180)), + pen=None + ) + self.resid_graph_imag = PlotItem( + x=[], y=[], + symbol='s', symbolPen=None, symbolBrush=mkBrush(color=(255, 127, 14)), + pen=None + ) self.resid_plot.addItem(self.resid_graph) self.resid_plot.addItem(self.resid_graph_imag) - self.data_graph = PlotItem(x=[], y=[], - symbol='o', symbolPen=None, symbolBrush=mkBrush(color=(31, 119, 180)), - pen=None) - self.data_graph_imag = PlotItem(x=[], y=[], - symbol='s', symbolPen=None, symbolBrush=mkBrush(color=(255, 127, 14)), - pen=None) + self.data_graph = PlotItem( + x=[], y=[], + symbol='o', symbolPen=None, symbolBrush=mkBrush(color=(31, 119, 180)), + pen=None + ) + self.data_graph_imag = PlotItem( + x=[], y=[], + symbol='s', symbolPen=None, symbolBrush=mkBrush(color=(255, 127, 14)), + pen=None + ) self.fit_plot.addItem(self.data_graph) self.fit_plot.addItem(self.data_graph_imag) @@ -85,13 +94,14 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): self.set_results(results) - def __call__(self, results: list): + def __call__(self, results: list, sub_colors: dict): self._previous_fits = {} self.sets_comboBox.blockSignals(True) self.sets_comboBox.clear() self.sets_comboBox.blockSignals(False) self._results = {} self._opts = {} + self.func_colors = sub_colors self.set_results(results) @@ -193,10 +203,11 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): self.fit_graph.setData(x=res.x, y=res.y.real) self.fit_graph_imag.setData(x=res.x, y=res.y.imag) - for i, f in enumerate(sub_funcs): - item = PlotItem(x=f.x, y=f.y.real, pen=mkPen({'color': i, 'style': 2})) + for f, c in zip(sub_funcs, self.func_colors[idx]): + col = mkColor(*[c_i*255 for c_i in c]) + item = PlotItem(x=f.x, y=f.y.real, pen=mkPen({'color': col, 'style': 2})) self.fit_plot.addItem(item) - item = PlotItem(x=f.x, y=f.y.imag, pen=mkPen({'color': i, 'style': 2})) + item = PlotItem(x=f.x, y=f.y.imag, pen=mkPen({'color': col, 'style': 2})) self.fit_plot.addItem(item) else: @@ -205,8 +216,8 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): self.fit_graph.setData(x=res.x, y=res.y) self.fit_graph_imag.setData(x=[], y=[]) - for i, f in enumerate(sub_funcs): - item = PlotItem(x=f.x, y=f.y, pen=mkPen({'color': i, 'style': 2})) + for f, c in zip(sub_funcs, self.func_colors[idx]): + item = PlotItem(x=f.x, y=f.y, pen=mkPen({'color': mkColor(*[c_i*255 for c_i in c]), 'style': 2})) self.fit_plot.addItem(item) self._plot_residuals(idx) diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 7f637d4..7f6123b 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -952,19 +952,19 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): for item in self.fit_dialog.preview_lines: g.add_external(item) - @QtCore.pyqtSlot(list) - def show_fit_results(self, results: list): + @QtCore.pyqtSlot(list, dict) + def show_fit_results(self, results: list, sub_colors: dict[str, tuple[float, float, float]]): self.fit_dialog.fit_button.setEnabled(True) self.fit_timer.stop() self.status.setText('') if results: if self.fitresult_dialog is None: - self.fitresult_dialog = QFitResult(results, self.management, parent=self) + self.fitresult_dialog = QFitResult(results, sub_colors, self.management, parent=self) self.fitresult_dialog.add_graphs(self.management.graphs.list()) self.fitresult_dialog.closed.connect(self.accepts_fit) self.fitresult_dialog.redoFit.connect(self.management.redo_fits) else: - self.fitresult_dialog(results) + self.fitresult_dialog(results, sub_colors) self.fitresult_dialog.add_graphs(self.management.graphs.list()) self.fitresult_dialog.show() diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index 4b959ee..a75cc2e 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -85,7 +85,7 @@ class UpperManagement(QtCore.QObject): newData = QtCore.pyqtSignal([list, str], [list, str, bool]) deleteData = QtCore.pyqtSignal(list) dataChanged = QtCore.pyqtSignal(str) - fitFinished = QtCore.pyqtSignal(list) + fitFinished = QtCore.pyqtSignal(list, dict) stopFit = QtCore.pyqtSignal() properties_collected = QtCore.pyqtSignal(dict) unset_state = QtCore.pyqtSignal(list) @@ -586,13 +586,23 @@ class UpperManagement(QtCore.QObject): def end_fit(self, result: list, success: bool): if success: logger.info('Successful fit') - self.fitFinished.emit(result) + + sub_colors = {} + for k, v in self.__fit_options[0].items(): + sub_colors.update({set_id: v['color'] for set_id in v['data_parameter']}) + + self.fitFinished.emit(result, sub_colors) + else: e = result[0] logger.exception(e, exc_info=True) - QtWidgets.QMessageBox.warning(QtWidgets.QWidget(), 'Fit failed', - f'Fit kaput with exception: \n\n{e!r}') - self.fitFinished.emit([]) + QtWidgets.QMessageBox.warning( + QtWidgets.QWidget(), + 'Fit failed', + f'Fit kaput with exception: \n\n{e!r}' + ) + self.fitFinished.emit([], {}) + self._fit_active = False @QtCore.pyqtSlot(dict) From 299bb043ea18247beb388d6f15ecc8e517e1bb10 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 27 Mar 2024 16:59:43 +0000 Subject: [PATCH 20/30] adjust log level for dockwidget (#258) Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/258 --- src/gui_qt/lib/logger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui_qt/lib/logger.py b/src/gui_qt/lib/logger.py index e1cb93c..346b638 100644 --- a/src/gui_qt/lib/logger.py +++ b/src/gui_qt/lib/logger.py @@ -135,6 +135,7 @@ class QTextHandler(logging.Handler): self.console = ConsoleDock(parent) self.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) + self.setLevel(logging.WARNING) def emit(self, record): msg = self.format(record) From 403273e0d739b83b0e876db245b087fcce08c2df Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 27 Mar 2024 18:02:53 +0000 Subject: [PATCH 21/30] emit update of changed graph titles (#259); close #257 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/259 --- src/gui_qt/data/signaledit/phase_dialog.py | 12 ++++++------ src/gui_qt/main/management.py | 1 + 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/gui_qt/data/signaledit/phase_dialog.py b/src/gui_qt/data/signaledit/phase_dialog.py index 601db39..d040312 100644 --- a/src/gui_qt/data/signaledit/phase_dialog.py +++ b/src/gui_qt/data/signaledit/phase_dialog.py @@ -413,12 +413,12 @@ class QPreviewDialog(QtWidgets.QDialog, Ui_ApodEdit): vb = self.time_graph.getPlotItem().getViewBox() vb.disableAutoRange(axis=vb.YAxis) - - self.zerofill_box.setVisible(self._all_time) - self.apod_box.setVisible(self._all_time) - self.shift_box.setVisible(self._all_time) - self.time_graph.setVisible(self._all_time) - self.logtime_widget.setVisible(self._all_time) + if self._all_time is not None: + self.zerofill_box.setVisible(self._all_time) + self.apod_box.setVisible(self._all_time) + self.shift_box.setVisible(self._all_time) + self.time_graph.setVisible(self._all_time) + self.logtime_widget.setVisible(self._all_time) self._temp_baseline = self._temp_baseline_time if self._all_time else self._temp_baseline_freq self._temp_fft = self._temp_fft_time if self._all_time else self._temp_fft_freq diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index a75cc2e..d334930 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -428,6 +428,7 @@ class UpperManagement(QtCore.QObject): self.graphs[d.graph].update_legend(identifier, name) elif identifier in self.graphs: self.graphs[identifier].title = name + self.graphs.valueChanged.emit() else: raise KeyError('Unknown ID ' + str(identifier)) From b25db92cf119e4c7013ef3ac31474fd260690660 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 2 Apr 2024 15:34:42 +0000 Subject: [PATCH 22/30] stop autoamtic ascii reading if first is cancelled (#260); close #251 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/260 --- src/gui_qt/io/asciireader.py | 2 +- src/gui_qt/io/filereaders.py | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/gui_qt/io/asciireader.py b/src/gui_qt/io/asciireader.py index 2520844..489c33d 100644 --- a/src/gui_qt/io/asciireader.py +++ b/src/gui_qt/io/asciireader.py @@ -178,7 +178,7 @@ class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader): @QtCore.pyqtSlot() def accept(self): if self.apply(): - self.close() + super().accept() def apply(self): # default row for x is the first row, it will be superseded if an integer number is given. diff --git a/src/gui_qt/io/filereaders.py b/src/gui_qt/io/filereaders.py index 11d28c2..808d96f 100755 --- a/src/gui_qt/io/filereaders.py +++ b/src/gui_qt/io/filereaders.py @@ -3,7 +3,7 @@ from __future__ import annotations from pathlib import Path import struct -from ..Qt import QtCore +from ..Qt import QtCore, QtWidgets from .asciireader import QAsciiReader from .hdfreader import QHdfViewer from .bdsreader import QBDSReader @@ -26,8 +26,12 @@ class QFileReader(QtCore.QObject): self.reader = {} for ext, reader in [ - ('txt', QAsciiReader), ('dsc', QDSCReader), ('agr', QGraceReader), - ('bds', QBDSReader), ('hdf', QHdfViewer), ('nmr', QNMRReader) + ('txt', QAsciiReader), + ('dsc', QDSCReader), + ('agr', QGraceReader), + ('bds', QBDSReader), + ('hdf', QHdfViewer), + ('nmr', QNMRReader), ]: self.register(ext, reader) @@ -57,7 +61,10 @@ class QFileReader(QtCore.QObject): try: # If QAsciiReader.skip = True it accepts automatically and returns None - r(f).exec() + status = r(f).exec() + if status == QtWidgets.QDialog.Rejected and isinstance(r, QAsciiReader): + break + except AttributeError: pass From c8aad904a8e125f381d3e4e9022c87d9d28a2484 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 3 Apr 2024 15:53:19 +0000 Subject: [PATCH 23/30] 262-column-header (#264); closes #262 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/264 --- src/gui_qt/io/asciireader.py | 5 ++++- src/gui_qt/io/filereaders.py | 5 +++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/gui_qt/io/asciireader.py b/src/gui_qt/io/asciireader.py index 489c33d..ea4c980 100644 --- a/src/gui_qt/io/asciireader.py +++ b/src/gui_qt/io/asciireader.py @@ -132,7 +132,10 @@ class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader): self.ascii_table.setHorizontalHeaderLabels(map(str, range(1, self.ascii_table.columnCount() + 1))) if self.column_checkBox.isChecked() and self.line_spinBox.isEnabled(): header_line = self.reader.header[self.line_spinBox.value()-1] - self.ascii_table.setHorizontalHeaderLabels(header_line.split()) + header_line = header_line.strip('\n\t\r, ') + header_line = re.sub(r'[\t ;,]+', ';', header_line) + + self.ascii_table.setHorizontalHeaderLabels(header_line.split(';')) @QtCore.pyqtSlot(int, name='on_staggered_checkBox_stateChanged') def changestaggeredrange(self, state: int): diff --git a/src/gui_qt/io/filereaders.py b/src/gui_qt/io/filereaders.py index 808d96f..59b491a 100755 --- a/src/gui_qt/io/filereaders.py +++ b/src/gui_qt/io/filereaders.py @@ -51,6 +51,7 @@ class QFileReader(QtCore.QObject): if not isinstance(fname, list): fname = [fname] + status = QtWidgets.QDialog.Accepted for f in fname: f = Path(f) dtype = self.guess_type(f) @@ -61,9 +62,9 @@ class QFileReader(QtCore.QObject): try: # If QAsciiReader.skip = True it accepts automatically and returns None - status = r(f).exec() - if status == QtWidgets.QDialog.Rejected and isinstance(r, QAsciiReader): + if status == QtWidgets.QDialog.DialogCode.Rejected and isinstance(r, QAsciiReader) and self.reader['txt'].skip: break + status = r(f).exec() except AttributeError: pass From 1162458290f1ff34e9994e1c9ce2889d5af247aa Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 4 Apr 2024 17:26:52 +0000 Subject: [PATCH 24/30] catch empty sets before fit (#265); closes #261 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/265 --- src/gui_qt/main/management.py | 4 ++-- src/nmreval/fit/data.py | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index d334930..56c387a 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -543,7 +543,7 @@ class UpperManagement(QtCore.QObject): else: d = fit_d.Data(_x[inside], _y[inside], we=we[inside], idx=set_id, complex_type=data_complex) except Exception as e: - raise Exception(f'Setting data failed for {set_id}') + raise Exception(f'Setting data failed for {data_i.name}') from e d.set_model(m) try: @@ -559,7 +559,7 @@ class UpperManagement(QtCore.QObject): return True except Exception as e: - logger.error('Fit preparation failed', *e.args) + logger.error(f'Fit preparation failed with error: {e.args}') QtWidgets.QMessageBox.warning(QtWidgets.QWidget(), 'Fit prep failed', f'Fit preparation failed:\n' diff --git a/src/nmreval/fit/data.py b/src/nmreval/fit/data.py index b6b94ff..7b39aa2 100644 --- a/src/nmreval/fit/data.py +++ b/src/nmreval/fit/data.py @@ -10,6 +10,9 @@ class Data: def __init__(self, x, y, we=None, idx=None, complex_type: int = 0): self.x = np.asarray(x) self.y = np.asarray(y) + if self.x.size == 0 or self.y.size == 0: + raise ValueError("Data is empty") + if self.y.shape[0] != self.x.shape[0]: raise ValueError(f'x and y have different lengths {self.x.shape[0]} and {self.y.shape[0]}') From 79d0ab162802adfea86aaf9ccc18dd2db0f38f85 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 29 Apr 2024 16:26:09 +0000 Subject: [PATCH 25/30] read-fid (#269); closes #268 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/269 --- src/gui_qt/io/asciireader.py | 44 ++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/gui_qt/io/asciireader.py b/src/gui_qt/io/asciireader.py index ea4c980..ea559c2 100644 --- a/src/gui_qt/io/asciireader.py +++ b/src/gui_qt/io/asciireader.py @@ -186,6 +186,7 @@ class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader): def apply(self): # default row for x is the first row, it will be superseded if an integer number is given. x = self.x_lineedit.text() + is_valid = True if x: try: x = int(x)-1 @@ -194,16 +195,35 @@ class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader): else: x = None + if not self.check_column_numbers(x, max(self.reader.width)): + _ = QtWidgets.QMessageBox.information(self, 'Improper input', + f'Input for x axis is invalid') + return False + try: y = [int(t)-1 for t in self.y_lineedit.text().split(' ')] except ValueError: y = None + if not self.check_column_numbers(y, max(self.reader.width)): + _ = QtWidgets.QMessageBox.information(self, 'Improper input', + f'Input for y axis is invalid') + return False + try: y_err = [int(t)-1 for t in self.deltay_lineEdit.text().split(' ')] except ValueError: y_err = None + mode = self.buttonGroup.checkedButton().text() + if mode != 'Points': + y_err = None + + if not self.check_column_numbers(y, max(self.reader.width)): + _ = QtWidgets.QMessageBox.information(self, 'Improper input', + f'Input for y_err axis is invalid') + return False + col_header = None if self.column_checkBox.isChecked(): col_header = [] @@ -221,15 +241,16 @@ class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader): x=x, y=y, yerr=y_err, - mode=self.buttonGroup.checkedButton().text(), + mode=mode, col_names=col_header, num_value=self.get_numerical_value(), ) self.data_read.emit(ret_dic) - except ImportError as e: + except Exception as e: _ = QtWidgets.QMessageBox.information(self, 'Reading failed', - f'Import data failed with {e.args}') + f'Import data failed with\n {e.args[0]}') + return False return True @@ -284,7 +305,7 @@ class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader): else: self.label_8.setText(fname) - def get_numerical_value(self): + def get_numerical_value(self) -> float: val = 0 if self.re_button.isChecked() and self._matches: m = self._matches[self.re_match_index.value()-1] @@ -295,3 +316,18 @@ class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader): val = float(self.custom_input.text()) return val + + def check_column_numbers(self, values: int | list[int] | None, num_column: int) -> bool: + is_valid = False + if values is None: + is_valid = True + elif isinstance(values, int): + is_valid = values < num_column + elif isinstance(values, list): + try: + is_valid = all(v < num_column for v in values) + except TypeError: + is_valid = False + + return is_valid + From 3ee7dca4572d803eeb4994839fba747141f87d15 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 30 Apr 2024 17:15:46 +0200 Subject: [PATCH 26/30] missed index as option --- src/gui_qt/io/asciireader.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/gui_qt/io/asciireader.py b/src/gui_qt/io/asciireader.py index ea559c2..5fb765b 100644 --- a/src/gui_qt/io/asciireader.py +++ b/src/gui_qt/io/asciireader.py @@ -317,10 +317,13 @@ class QAsciiReader(QtWidgets.QDialog, Ui_ascii_reader): return val - def check_column_numbers(self, values: int | list[int] | None, num_column: int) -> bool: + def check_column_numbers(self, values: int | list[int] | str | None, num_column: int) -> bool: is_valid = False if values is None: is_valid = True + elif values == 'index': + is_valid = True + elif isinstance(values, int): is_valid = values < num_column elif isinstance(values, list): From 8f92d8d822875c0ce3e0f58f435f2253b100a30e Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 24 Jun 2024 15:59:33 +0000 Subject: [PATCH 27/30] dev (#275) closes issues #267 #274, #255, #256 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/275 --- src/gui_qt/_py/basewindow.py | 21 +++++++--- src/gui_qt/_py/interpol_dialog.py | 8 ++-- src/gui_qt/data/container.py | 28 ++++++++----- src/gui_qt/data/valueeditwidget.py | 4 +- src/gui_qt/io/fcbatchreader.py | 8 ++++ src/gui_qt/main/mainwindow.py | 31 ++++++++------ src/gui_qt/main/management.py | 13 ++++-- src/gui_qt/math/interpol.py | 60 +++++++++++++++++++++------- src/nmreval/data/points.py | 29 +++++++++----- src/nmreval/models/basic.py | 12 ++++++ src/resources/_ui/basewindow.ui | 31 +++++++++++--- src/resources/_ui/interpol_dialog.ui | 37 +---------------- 12 files changed, 180 insertions(+), 102 deletions(-) diff --git a/src/gui_qt/_py/basewindow.py b/src/gui_qt/_py/basewindow.py index d916548..8d23235 100644 --- a/src/gui_qt/_py/basewindow.py +++ b/src/gui_qt/_py/basewindow.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'src/resources/_ui/basewindow.ui' +# Form implementation generated from reading ui file './nmreval/src/resources/_ui/basewindow.ui' # # Created by: PyQt5 UI code generator 5.15.10 # @@ -87,6 +87,8 @@ class Ui_BaseWindow(object): self.menuSave.setObjectName("menuSave") self.menuData = QtWidgets.QMenu(self.menubar) self.menuData.setObjectName("menuData") + self.menuCut_to_visible_range = QtWidgets.QMenu(self.menuData) + self.menuCut_to_visible_range.setObjectName("menuCut_to_visible_range") self.menuHelp = QtWidgets.QMenu(self.menubar) self.menuHelp.setObjectName("menuHelp") self.menuExtra = QtWidgets.QMenu(self.menubar) @@ -304,8 +306,6 @@ class Ui_BaseWindow(object): self.actionDerivation.setObjectName("actionDerivation") self.actionIntegration = QtWidgets.QAction(BaseWindow) self.actionIntegration.setObjectName("actionIntegration") - self.action_cut = QtWidgets.QAction(BaseWindow) - self.action_cut.setObjectName("action_cut") self.actionMove_between_plots = QtWidgets.QAction(BaseWindow) self.actionMove_between_plots.setObjectName("actionMove_between_plots") self.actionBaseline = QtWidgets.QAction(BaseWindow) @@ -368,6 +368,10 @@ class Ui_BaseWindow(object): self.actionExclude_region = QtWidgets.QAction(BaseWindow) self.actionExclude_region.setCheckable(True) self.actionExclude_region.setObjectName("actionExclude_region") + self.action_cut_xaxis = QtWidgets.QAction(BaseWindow) + self.action_cut_xaxis.setObjectName("action_cut_xaxis") + self.action_cut_yaxis = QtWidgets.QAction(BaseWindow) + self.action_cut_yaxis.setObjectName("action_cut_yaxis") self.menuSave.addAction(self.actionSave) self.menuSave.addAction(self.actionExportGraphic) self.menuSave.addAction(self.action_save_fit_parameter) @@ -380,6 +384,9 @@ class Ui_BaseWindow(object): self.menuFile.addSeparator() self.menuFile.addAction(self.action_close) self.menuFile.addSeparator() + self.menuCut_to_visible_range.addSeparator() + self.menuCut_to_visible_range.addAction(self.action_cut_xaxis) + self.menuCut_to_visible_range.addAction(self.action_cut_yaxis) self.menuData.addAction(self.action_new_set) self.menuData.addAction(self.action_delete_sets) self.menuData.addAction(self.actionMove_between_plots) @@ -389,7 +396,7 @@ class Ui_BaseWindow(object): self.menuData.addAction(self.action_sort_pts) self.menuData.addAction(self.actionSkip_points) self.menuData.addSeparator() - self.menuData.addAction(self.action_cut) + self.menuData.addAction(self.menuCut_to_visible_range.menuAction()) self.menuData.addSeparator() self.menuData.addAction(self.actionChange_datatypes) self.menuHelp.addAction(self.actionShow_error_log) @@ -515,6 +522,7 @@ class Ui_BaseWindow(object): self.menuFile.setTitle(_translate("BaseWindow", "&File")) self.menuSave.setTitle(_translate("BaseWindow", "&Save...")) self.menuData.setTitle(_translate("BaseWindow", "&Data")) + self.menuCut_to_visible_range.setTitle(_translate("BaseWindow", "Cut to visible range")) self.menuHelp.setTitle(_translate("BaseWindow", "&Help")) self.menuExtra.setTitle(_translate("BaseWindow", "Math")) self.menuNormalize.setTitle(_translate("BaseWindow", "&Normalize")) @@ -608,7 +616,6 @@ class Ui_BaseWindow(object): self.actionIntegrate.setText(_translate("BaseWindow", "Integrate")) self.actionDerivation.setText(_translate("BaseWindow", "Differentiation...")) self.actionIntegration.setText(_translate("BaseWindow", "Integration...")) - self.action_cut.setText(_translate("BaseWindow", "Cut to visible range")) self.actionMove_between_plots.setText(_translate("BaseWindow", "Move sets...")) self.actionBaseline.setText(_translate("BaseWindow", "Baseline...")) self.actionCalculateT1.setText(_translate("BaseWindow", "Calculate relaxation...")) @@ -636,6 +643,10 @@ class Ui_BaseWindow(object): self.actionBinning.setText(_translate("BaseWindow", "Binning...")) self.actionTNMH.setText(_translate("BaseWindow", "TNMH...")) self.actionExclude_region.setText(_translate("BaseWindow", "Exclude region")) + self.action_cut_xaxis.setText(_translate("BaseWindow", "x axis")) + self.action_cut_xaxis.setToolTip(_translate("BaseWindow", "Remove data points outside visible x range.")) + self.action_cut_yaxis.setText(_translate("BaseWindow", "y axis")) + self.action_cut_yaxis.setToolTip(_translate("BaseWindow", "Remove data points outside visible y range. Uses real part of points.")) from ..data.datawidget.datawidget import DataWidget from ..data.integral_widget import IntegralWidget from ..data.point_select import PointSelectWidget diff --git a/src/gui_qt/_py/interpol_dialog.py b/src/gui_qt/_py/interpol_dialog.py index c3b0827..c0ef858 100644 --- a/src/gui_qt/_py/interpol_dialog.py +++ b/src/gui_qt/_py/interpol_dialog.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'src/resources/_ui/interpol_dialog.ui' +# Form implementation generated from reading ui file './nmreval/src/resources/_ui/interpol_dialog.ui' # -# Created by: PyQt5 UI code generator 5.15.9 +# Created by: PyQt5 UI code generator 5.15.10 # # 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. @@ -55,7 +55,7 @@ class Ui_Dialog(object): self.gridLayout.addWidget(self.interp_comboBox, 4, 1, 1, 1) self.buttonBox = QtWidgets.QDialogButtonBox(Dialog) self.buttonBox.setOrientation(QtCore.Qt.Horizontal) - self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) + self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Apply|QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) self.buttonBox.setObjectName("buttonBox") self.gridLayout.addWidget(self.buttonBox, 12, 0, 1, 2) self.line = QtWidgets.QFrame(Dialog) @@ -132,8 +132,6 @@ class Ui_Dialog(object): self.label_8.setBuddy(self.dest_combobox) self.retranslateUi(Dialog) - self.buttonBox.accepted.connect(Dialog.accept) # type: ignore - self.buttonBox.rejected.connect(Dialog.reject) # type: ignore QtCore.QMetaObject.connectSlotsByName(Dialog) Dialog.setTabOrder(self.listWidget, self.ylog_checkBox) Dialog.setTabOrder(self.ylog_checkBox, self.interp_comboBox) diff --git a/src/gui_qt/data/container.py b/src/gui_qt/data/container.py index 71bf9f4..7364579 100644 --- a/src/gui_qt/data/container.py +++ b/src/gui_qt/data/container.py @@ -300,10 +300,12 @@ class ExperimentContainer(QtCore.QObject): self._relations.pop(relation_type) def _update_actions(self): - self.actions.update({'sort': self._data.sort, - 'cut': self._data.cut, - 'norm': self._data.normalize, - 'center': self.center}) + self.actions.update({ + 'sort': self._data.sort, + 'cut': self._data.cut, + 'norm': self._data.normalize, + 'center': self.center, + }) @plot_update def update(self, opts: dict): @@ -311,9 +313,11 @@ class ExperimentContainer(QtCore.QObject): def get_properties(self) -> dict: props = OrderedDict() - props['General'] = OrderedDict([('Name', self.name), - ('Value', str(self.value)), - ('Group', str(self.group))]) + props['General'] = OrderedDict([ + ('Name', self.name), + ('Value', str(self.value)), + ('Group', str(self.group)), + ]) props['Symbol'] = OrderedDict() props['Line'] = OrderedDict() @@ -480,10 +484,12 @@ class ExperimentContainer(QtCore.QObject): else: prefix = f'g[{i}].s[{j}].' - namespace = {prefix + 'x': (self.x, 'x values'), - prefix + 'y': [self.y, 'y values'], - prefix + 'y_err': (self.y_err, 'y error values'), - prefix + 'value': (self.value, str(self.value))} + namespace = { + prefix + 'x': (self.x, 'x values'), + prefix + 'y': [self.y, 'y values'], + prefix + 'y_err': (self.y_err, 'y error values'), + prefix + 'value': (self.value, str(self.value)), + } if len(self._fits) == 1: namespace.update({ diff --git a/src/gui_qt/data/valueeditwidget.py b/src/gui_qt/data/valueeditwidget.py index ae95a5f..de2376b 100644 --- a/src/gui_qt/data/valueeditwidget.py +++ b/src/gui_qt/data/valueeditwidget.py @@ -385,6 +385,6 @@ class ValueModel(QtCore.QAbstractTableModel): @staticmethod def as_string(value) -> str: if isinstance(value, complex): - return f'{value.real:.8g}{value.imag:+.8g}j' + return f'{value.real:.13g}{value.imag:+.13g}j' else: - return f'{value:.8g}' + return f'{value:.13g}' diff --git a/src/gui_qt/io/fcbatchreader.py b/src/gui_qt/io/fcbatchreader.py index c4bb80b..a4444e6 100644 --- a/src/gui_qt/io/fcbatchreader.py +++ b/src/gui_qt/io/fcbatchreader.py @@ -28,6 +28,12 @@ class QFCReader(QtWidgets.QDialog, Ui_FCEval_dialog): self.listWidget.installEventFilter(self) + def __call__(self, path=None): + if path is None: + path = pathlib.Path().home() + self.path = path + self.listWidget.clear() + def eventFilter(self, src: QtCore.QObject, evt: QtCore.QEvent) -> bool: # intercept key press in listwidget to allow deletion with Del if evt.type() == QtCore.QEvent.Type.KeyPress: @@ -82,6 +88,7 @@ class QFCReader(QtWidgets.QDialog, Ui_FCEval_dialog): def accept(self): items = [self.listWidget.item(i).text() for i in range(self.listWidget.count())] + print(items) if items: with busy_cursor(): self.read(items) @@ -116,6 +123,7 @@ class QFCReader(QtWidgets.QDialog, Ui_FCEval_dialog): ret_vals = [] ret_vals.extend(fc_eval.get_parameter(path=self.label.text(), kind='temp', parameter=save_variables)) + print(ret_vals) grp = '' if not self.graph_checkbox.isChecked(): diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 7f6123b..34e94ba 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -62,6 +62,8 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.fitresult_dialog = None self.eval = None self.editor = None + self._interpol_dialog = None + self.fc_reader = None self.logtext = QTextHandler(self) logger.addHandler(self.logtext) @@ -233,7 +235,8 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.action_norm_first.triggered.connect(lambda: self.management.apply('norm', ('first',))) self.action_norm_last.triggered.connect(lambda: self.management.apply('norm', ('last',))) self.action_norm_area.triggered.connect(lambda: self.management.apply('norm', ('area',))) - self.action_cut.triggered.connect(lambda: self.management.cut()) + self.action_cut_xaxis.triggered.connect(lambda: self.management.cut(True, False)) + self.action_cut_yaxis.triggered.connect(lambda: self.management.cut(False, True)) self.actionConcatenate_sets.triggered.connect(lambda: self.management.cat()) @@ -264,14 +267,15 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): @QtCore.pyqtSlot(name='on_actionOpen_FC_triggered') def read_fc(self): - reader = QFCReader(path=self.path, parent=self) - reader.add_graphs(self.management.graphs.list()) - reader.data_read.connect(self.management.add_new_data) - reader.exec() + if self.fc_reader is None: + self.fc_reader = QFCReader(path=self.path, parent=self) + self.fc_reader.data_read.connect(self.management.add_new_data) + else: + self.fc_reader(path=self.path) + self.fc_reader.add_graphs(self.management.graphs.list()) + self.fc_reader.exec() - self.path = reader.path - - del reader + self.path = self.fc_reader.path @QtCore.pyqtSlot(name='on_actionPrint_triggered') def print(self): @@ -701,10 +705,13 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): return gnames = self.management.graphs.tree() - dialog = InterpolDialog(parent=self) - dialog.set_data(gnames, self.current_graph_widget.id) - dialog.new_data.connect(self.management.interpolate_data) - dialog.show() + if self._interpol_dialog is None: + self._interpol_dialog = InterpolDialog(parent=self) + self._interpol_dialog.new_data.connect(self.management.interpolate_data) + else: + self._interpol_dialog() + self._interpol_dialog.set_data(gnames, self.current_graph_widget.id) + self._interpol_dialog.show() @QtCore.pyqtSlot(name='on_action_calc_triggered') def open_eval_dialog(self): diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index 56c387a..f47362b 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -450,10 +450,17 @@ class UpperManagement(QtCore.QObject): self.undostack.push(single_undo) self.undostack.endMacro() - def cut(self): + def cut(self, x: bool = False, y: bool = False) -> None: if self.current_graph: - xlim, _ = self.graphs[self.current_graph].ranges - self.apply('cut', xlim) + xlim, ylim = self.graphs[self.current_graph].ranges + + if x is False: + xlim = (None, None) + + if y is False: + ylim = (None, None) + + self.apply('cut', (*xlim, *ylim)) @QtCore.pyqtSlot() def unmask(self): diff --git a/src/gui_qt/math/interpol.py b/src/gui_qt/math/interpol.py index c58d03c..046a7bc 100644 --- a/src/gui_qt/math/interpol.py +++ b/src/gui_qt/math/interpol.py @@ -16,6 +16,12 @@ class InterpolDialog(QtWidgets.QDialog, Ui_Dialog): self.step_lineEdit.setValidator(QtGui.QIntValidator()) self._data = {} + self._src_id = None + self._dest_graph = '' + + def __call__(self): + self.listWidget.clear() + self._data = {} @QtCore.pyqtSlot(int, name='on_xaxis_comboBox_currentIndexChanged') def change_x_source(self, idx: int): @@ -25,29 +31,41 @@ class InterpolDialog(QtWidgets.QDialog, Ui_Dialog): def set_data(self, data, current_gid): self.graph_combobox.blockSignals(True) self._data = {} + dest_idx = 0 for (gid, graph_name), sets in data.items(): self.graph_combobox.addItem(graph_name, userData=gid) self.dest_combobox.addItem(graph_name, userData=gid) + if self._dest_graph == gid: + dest_idx = self.dest_combobox.currentIndex() if gid == current_gid: self.make_list(sets) self._data[gid] = sets self.graph_combobox.blockSignals(False) - self.change_graph(0) + self.change_graph(dest_idx) def make_list(self, current_sets): for sid, set_name in current_sets: item = QtWidgets.QListWidgetItem(set_name) - item.setData(QtCore.Qt.UserRole, sid) - item.setCheckState(QtCore.Qt.Checked) + item.setData(QtCore.Qt.ItemDataRole.UserRole, sid) + item.setCheckState(QtCore.Qt.CheckState.Checked) self.listWidget.addItem(item) @QtCore.pyqtSlot(int, name='on_graph_combobox_currentIndexChanged') def change_graph(self, idx: int): self.set_combobox.clear() - gid = self.graph_combobox.itemData(idx, QtCore.Qt.UserRole) + gid = self.graph_combobox.itemData(idx, QtCore.Qt.ItemDataRole.UserRole) + set_idx = -1 if gid is not None: - for set_key, set_name in self._data[gid]: + for i, (set_key, set_name) in enumerate(self._data[gid]): + print(self._src_id, set_key, set_name, i) self.set_combobox.addItem(set_name, userData=set_key) + print(self.set_combobox.currentIndex()) + if self._src_id == set_key: + set_idx = i + + print(set_idx) + if set_idx > -1: + self.set_combobox.setCurrentIndex(set_idx) def collect_parameter(self): xlog = self.xlog_checkBox.isChecked() @@ -71,21 +89,35 @@ class InterpolDialog(QtWidgets.QDialog, Ui_Dialog): x_src = (start, stop, step, loggy) else: - x_src = (self.set_combobox.currentData(QtCore.Qt.UserRole),) + self._src_id = self.set_combobox.currentData(QtCore.Qt.ItemDataRole.UserRole) + x_src = (self._src_id,) - dest_graph = self.dest_combobox.currentData(QtCore.Qt.UserRole) + self._dest_graph = self.dest_combobox.currentData(QtCore.Qt.ItemDataRole.UserRole) use_data = [] for i in range(self.listWidget.count()): item = self.listWidget.item(i) - if item.checkState() == QtCore.Qt.Checked: - use_data.append(item.data(QtCore.Qt.UserRole)) + if item.checkState() == QtCore.Qt.CheckState.Checked: + use_data.append(item.data(QtCore.Qt.ItemDataRole.UserRole)) - self.new_data.emit(use_data, mode, xlog, ylog, x_src, dest_graph) + self.new_data.emit(use_data, mode, xlog, ylog, x_src, self._dest_graph) return True - def accept(self): - success = self.collect_parameter() - if success: - super().accept() + def _save_state(self): + self._src_id = self.set_combobox.currentData(QtCore.Qt.ItemDataRole.UserRole) + self._dest_graph = self.dest_combobox.currentData(QtCore.Qt.ItemDataRole.UserRole) + + @QtCore.pyqtSlot(QtWidgets.QAbstractButton, name='on_buttonBox_clicked') + def check_next_actions(self, bttn: QtWidgets.QAbstractButton): + role = self.buttonBox.buttonRole(bttn) + self._save_state() + + if role == self.buttonBox.ButtonRole.RejectRole: + self.close() + else: + success = self.collect_parameter() + + if success and role == self.buttonBox.ButtonRole.AcceptRole: + self.close() + diff --git a/src/nmreval/data/points.py b/src/nmreval/data/points.py index 8332084..1f891ae 100644 --- a/src/nmreval/data/points.py +++ b/src/nmreval/data/points.py @@ -540,26 +540,37 @@ class Points: return self - def cut(self, low_lim: float = None, high_lim: float = None): + def cut(self, x_low: float = None, x_high: float = None, y_low: float = None, y_high: float = None): """ Cut Args: - low_lim: - high_lim: + x_low: Lower limit + x_high: Upper limit for x values + y_low: Lower limit + y_high: Upper limit for x valuew Returns: """ - if low_lim is None and high_lim is None: + + if x_low is None and x_high is None and y_low is None and y_high is None: return self - if low_lim is None: - low_lim = np.min(self._x) + if x_low is None: + x_low = np.min(self._x)-1 - if high_lim is None: - high_lim = np.max(self._x) + if x_high is None: + x_high = np.max(self._x)+1 - _mask = np.ma.masked_inside(self._x, low_lim, high_lim).mask + if y_low is None: + y_low = np.min(self._y.real)-1 + + if y_high is None: + y_high = np.max(self._y.real)+1 + + x_mask = (self._x >= x_low) & (self._x <= x_high) + y_mask = (self._y.real >= y_low) & (self._y.real <= y_high) + _mask = x_mask & y_mask self._x = self._x[_mask] self._y = self._y[_mask] diff --git a/src/nmreval/models/basic.py b/src/nmreval/models/basic.py index 411e218..ce4308d 100644 --- a/src/nmreval/models/basic.py +++ b/src/nmreval/models/basic.py @@ -191,6 +191,18 @@ class PowerLawCross: return ret_val +class Sinc: + type = 'Basic' + name = 'Sinc' + equation = 'C * sinc((x-x_{0})/w)' + params = ['C', 'x_{0}', 'w'] + + @staticmethod + def func(x, c: float, x0: float, w: float): + # numpy sinc is defined as sin(pi*x)/(pi*x) + return c * np.sinc(((x-x0)/w)/np.pi) + + class Sine: """ Wavy sine function diff --git a/src/resources/_ui/basewindow.ui b/src/resources/_ui/basewindow.ui index 01987b8..85c1f68 100644 --- a/src/resources/_ui/basewindow.ui +++ b/src/resources/_ui/basewindow.ui @@ -172,6 +172,14 @@ &Data + + + Cut to visible range + + + + + @@ -181,7 +189,7 @@ - + @@ -862,11 +870,6 @@ Integration... - - - Cut to visible range - - Move sets... @@ -1030,6 +1033,22 @@ Exclude region + + + x axis + + + Remove data points outside visible x range. + + + + + y axis + + + Remove data points outside visible y range. Uses real part of points. + + diff --git a/src/resources/_ui/interpol_dialog.ui b/src/resources/_ui/interpol_dialog.ui index 28fa858..865f70b 100644 --- a/src/resources/_ui/interpol_dialog.ui +++ b/src/resources/_ui/interpol_dialog.ui @@ -119,7 +119,7 @@ Qt::Horizontal - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok
@@ -300,38 +300,5 @@ dest_combobox - - - buttonBox - accepted() - Dialog - accept() - - - 251 - 490 - - - 157 - 274 - - - - - buttonBox - rejected() - Dialog - reject() - - - 319 - 490 - - - 286 - 274 - - - - + From a2a95e796af4b43eb85ce9675457b6b499fdd0dd Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 4 Jul 2024 10:37:43 +0000 Subject: [PATCH 28/30] wrong addition to parameter length in iterator (#278) fixes #277 Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/278 --- src/gui_qt/fit/fitwindow.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index a5a4fd0..443e894 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -495,7 +495,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): if model_p['active']: cnt += self.param_widgets[model_p['cnt']].set_parameter(fit_id, param[cnt:]) if model_p['children']: - cnt += self.set_parameter_iter(fit_id, param, model_p['children'], cnt=cnt) + cnt = self.set_parameter_iter(fit_id, param, model_p['children'], cnt=cnt) return cnt From 5823ddd18c757eacf82603034273ad96ae002af1 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Fri, 5 Jul 2024 17:31:58 +0000 Subject: [PATCH 29/30] preview preparation needs to pass list of active data to children (#280) Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/280 --- src/gui_qt/fit/fitwindow.py | 12 +++++++++--- src/gui_qt/main/mainwindow.py | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/gui_qt/fit/fitwindow.py b/src/gui_qt/fit/fitwindow.py index 443e894..6700493 100644 --- a/src/gui_qt/fit/fitwindow.py +++ b/src/gui_qt/fit/fitwindow.py @@ -219,8 +219,14 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): if len(self.models) == 1: self.model_frame.hide() - def _prepare(self, model: list, function_use: list = None, - parameter: dict = None, add_idx: bool = False, cnt: int = 0) -> tuple[dict, int]: + def _prepare( + self, + model: list, + function_use: list = None, + parameter: dict = None, + add_idx: bool = False, + cnt: int = 0, + ) -> tuple[dict, int]: if parameter is None: parameter = { @@ -263,7 +269,7 @@ class QFitDialog(QtWidgets.QWidget, Ui_FitDialog): if f['children']: # recurse for children - _, cnt = self._prepare(f['children'], parameter=parameter, add_idx=add_idx, cnt=cnt) + _, cnt = self._prepare(f['children'], parameter=parameter, function_use=function_use, add_idx=add_idx, cnt=cnt) return parameter, cnt diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 34e94ba..4851d40 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -12,7 +12,7 @@ from nmreval.lib.logger import logger from nmreval.io.sessionwriter import NMRWriter from .management import UpperManagement -from ..lib.logger import ConsoleDock, QTextHandler +from ..lib.logger import QTextHandler from ..Qt import QtGui, QtPrintSupport from ..data.shift_graphs import QShift from ..data.signaledit import QPreviewDialog, QBaselineDialog From e1b76e837dad46757e2ca622c3f4123f52210210 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 16 Jul 2024 17:01:20 +0000 Subject: [PATCH 30/30] dev (#283) Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/283 --- src/gui_qt/data/datawidget/datawidget.py | 13 ++++++++++--- src/gui_qt/fit/fit_toolbar.py | 2 +- src/gui_qt/main/mainwindow.py | 10 +++++++--- src/gui_qt/main/management.py | 14 +++++++++----- src/nmreval/distributions/energy.py | 7 ++++++- 5 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/gui_qt/data/datawidget/datawidget.py b/src/gui_qt/data/datawidget/datawidget.py index e24c10d..23c1451 100644 --- a/src/gui_qt/data/datawidget/datawidget.py +++ b/src/gui_qt/data/datawidget/datawidget.py @@ -17,7 +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) + extendFits = QtCore.pyqtSignal(list, bool) # noinspection PyUnresolvedReferences def __init__(self, parent=None): @@ -465,7 +465,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 = save_action = extend_action = None + plt_action = save_action = extend_action = subfit_action = None menu.addSeparator() col_menu = menu.addMenu('Color cycle') for c in available_cycles.keys(): @@ -497,6 +497,7 @@ class DataTree(QtWidgets.QTreeWidget): plt_action = menu.addAction('Plot fit parameter') save_action = menu.addAction('Save fit parameter') extend_action = menu.addAction('Extrapolate fit') + subfit_action = menu.addAction('Plot partial functions') action = menu.exec(evt.globalPos()) @@ -504,6 +505,9 @@ class DataTree(QtWidgets.QTreeWidget): for gid, sets in idx.items(): s.extend(sets) + if action is None: + return + if action == del_action: self.management.delete_sets(s) @@ -521,7 +525,10 @@ class DataTree(QtWidgets.QTreeWidget): self.saveFits.emit(s) elif action == extend_action: - self.extendFits.emit(s) + self.extendFits.emit(s, False) + + elif action == subfit_action: + self.extendFits.emit(s, True) elif action.parent() == col_menu: self.management.set_cycle(s, action.text()) diff --git a/src/gui_qt/fit/fit_toolbar.py b/src/gui_qt/fit/fit_toolbar.py index 283c335..74ed526 100644 --- a/src/gui_qt/fit/fit_toolbar.py +++ b/src/gui_qt/fit/fit_toolbar.py @@ -10,7 +10,7 @@ class FitToolbar(QtWidgets.QToolBar): limit_menu: QtWidgets.QMenu, parent=None, ): - super().__init__(parent=parent) + super().__init__('Fit', parent=parent) self.fit_action = fitaction self.region = RegionItem() diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 4851d40..713eb75 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -991,15 +991,19 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.editor.setWindowModality(QtCore.Qt.WindowModality.ApplicationModal) self.editor.show() - @QtCore.pyqtSlot(list) - def extend_fit(self, sets: list): + @QtCore.pyqtSlot(list, bool) + def extend_fit(self, sets: list, only_subplots: bool): + if only_subplots: + self.management.extend_fits(sets, None, True) + return + w = FitExtension(self) res = w.exec() if res: p = w.values spacefunc = geomspace if p[3] else linspace x = spacefunc(p[0], p[1], num=p[2]) - self.management.extend_fits(sets, x) + self.management.extend_fits(sets, x, False) @QtCore.pyqtSlot(name='on_action_create_fit_function_triggered') def open_fitmodel_wizard(self): diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index f47362b..21715fa 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -711,17 +711,21 @@ class UpperManagement(QtCore.QObject): self.newData.emit(f_id_list, gid) - def extend_fits(self, set_id: list, x_range: np.ndarray): + def extend_fits(self, set_id: list, x_range: np.ndarray | None, only_subplots: bool): 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) + data = fit = self[sid] graph_id = data.graph if graph_id not in graphs: graphs[graph_id] = [] - graphs[graph_id].append(self.add(fit)) + + if not only_subplots: + fit = data.copy(full=True, keep_color=True) + if x_range is not None: + fit.data = fit.data.with_new_x(x_range) + + 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)): diff --git a/src/nmreval/distributions/energy.py b/src/nmreval/distributions/energy.py index 40f4c7f..059b2a5 100644 --- a/src/nmreval/distributions/energy.py +++ b/src/nmreval/distributions/energy.py @@ -3,7 +3,12 @@ from ctypes import c_double, cast, pointer, c_void_p import numpy as np from scipy import LowLevelCallable -from scipy.integrate import quad, simps as simpson + +from scipy.integrate import quad +try: + from scipy.integrate import simps as simpson +except ImportError: + from scipy.integrate import simpson from .base import Distribution from ..lib.utils import ArrayLike