diff --git a/AppImageBuilder.yml b/AppImageBuilder.yml index 084c2e8..ffd4159 100644 --- a/AppImageBuilder.yml +++ b/AppImageBuilder.yml @@ -32,7 +32,7 @@ AppDir: arch: amd64 allow_unauthenticated: true sources: - - sourceline: 'deb [arch=amd64] http://mirror.infra.pkm/ bullseye main contrib non-free' + - sourceline: 'deb [arch=amd64] http://ftp.uni-mainz.de/debian bullseye main contrib non-free' include: # for /usr/bin/env @@ -42,7 +42,7 @@ AppDir: # - hicolor-icon-theme - libatlas3-base - gnuplot-nox - - python3.9-minimal + - python3-minimal - python3-numpy - python3-scipy - python3-bsddb3 diff --git a/src/gui_qt/_py/apod_dialog.py b/src/gui_qt/_py/apod_dialog.py index 3c985d9..6a40632 100644 --- a/src/gui_qt/_py/apod_dialog.py +++ b/src/gui_qt/_py/apod_dialog.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file '_ui/apod_dialog.ui' +# Form implementation generated from reading ui file 'src/resources/_ui/apod_dialog.ui' # -# Created by: PyQt5 UI code generator 5.12.3 +# Created by: PyQt5 UI code generator 5.15.9 # -# 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 @@ -23,7 +24,7 @@ class Ui_ApodEdit(object): self.gridLayout.setContentsMargins(3, 3, 3, 3) self.gridLayout.setSpacing(3) self.gridLayout.setObjectName("gridLayout") - self.graphicsView = PlotWidget(ApodEdit) + self.graphicsView = NMRPlotWidget(ApodEdit) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -31,7 +32,7 @@ class Ui_ApodEdit(object): self.graphicsView.setSizePolicy(sizePolicy) self.graphicsView.setObjectName("graphicsView") self.gridLayout.addWidget(self.graphicsView, 2, 0, 1, 1) - self.graphicsView_2 = PlotWidget(ApodEdit) + self.graphicsView_2 = NMRPlotWidget(ApodEdit) self.graphicsView_2.setObjectName("graphicsView_2") self.gridLayout.addWidget(self.graphicsView_2, 2, 1, 1, 1) self.apodcombobox = QtWidgets.QComboBox(ApodEdit) @@ -63,12 +64,12 @@ class Ui_ApodEdit(object): self.gridLayout.addWidget(self.eqn_label, 0, 1, 1, 1) self.retranslateUi(ApodEdit) - self.buttonBox.accepted.connect(ApodEdit.accept) - self.buttonBox.rejected.connect(ApodEdit.close) + self.buttonBox.accepted.connect(ApodEdit.accept) # type: ignore + self.buttonBox.rejected.connect(ApodEdit.close) # type: ignore QtCore.QMetaObject.connectSlotsByName(ApodEdit) def retranslateUi(self, ApodEdit): _translate = QtCore.QCoreApplication.translate ApodEdit.setWindowTitle(_translate("ApodEdit", "Apodization")) self.eqn_label.setText(_translate("ApodEdit", "TextLabel")) -from pyqtgraph import PlotWidget +from ..lib.graph_items import NMRPlotWidget diff --git a/src/gui_qt/_py/baseline_dialog.py b/src/gui_qt/_py/baseline_dialog.py index ca67fe7..6c632f7 100644 --- a/src/gui_qt/_py/baseline_dialog.py +++ b/src/gui_qt/_py/baseline_dialog.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file '_ui/baseline_dialog.ui' +# Form implementation generated from reading ui file 'src/resources/_ui/baseline_dialog.ui' # -# Created by: PyQt5 UI code generator 5.12.3 +# Created by: PyQt5 UI code generator 5.15.9 # -# 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 @@ -44,7 +45,7 @@ class Ui_SignalEdit(object): self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) self.buttonBox.setObjectName("buttonBox") self.gridLayout.addWidget(self.buttonBox, 1, 0, 1, 3) - self.graphicsView = PlotWidget(SignalEdit) + self.graphicsView = NMRPlotWidget(SignalEdit) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -54,11 +55,11 @@ class Ui_SignalEdit(object): self.gridLayout.addWidget(self.graphicsView, 0, 2, 1, 1) self.retranslateUi(SignalEdit) - self.buttonBox.accepted.connect(SignalEdit.accept) - self.buttonBox.rejected.connect(SignalEdit.close) + self.buttonBox.accepted.connect(SignalEdit.accept) # type: ignore + self.buttonBox.rejected.connect(SignalEdit.close) # type: ignore QtCore.QMetaObject.connectSlotsByName(SignalEdit) def retranslateUi(self, SignalEdit): _translate = QtCore.QCoreApplication.translate SignalEdit.setWindowTitle(_translate("SignalEdit", "Dialog")) -from pyqtgraph import PlotWidget +from ..lib.graph_items import NMRPlotWidget diff --git a/src/gui_qt/_py/dscfile_dialog.py b/src/gui_qt/_py/dscfile_dialog.py index a0f3242..1e84afc 100644 --- a/src/gui_qt/_py/dscfile_dialog.py +++ b/src/gui_qt/_py/dscfile_dialog.py @@ -166,7 +166,7 @@ class Ui_Dialog(object): self.gridLayout = QtWidgets.QGridLayout(self.layoutWidget) self.gridLayout.setContentsMargins(0, 0, 0, 0) self.gridLayout.setObjectName("gridLayout") - self.raw_graph = PlotWidget(self.layoutWidget) + self.raw_graph = NMRPlotWidget(self.layoutWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -175,7 +175,7 @@ class Ui_Dialog(object): self.raw_graph.setMinimumSize(QtCore.QSize(300, 200)) self.raw_graph.setObjectName("raw_graph") self.gridLayout.addWidget(self.raw_graph, 0, 0, 1, 1) - self.calib_graph = PlotWidget(self.layoutWidget) + self.calib_graph = NMRPlotWidget(self.layoutWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -184,7 +184,7 @@ class Ui_Dialog(object): self.calib_graph.setMinimumSize(QtCore.QSize(300, 200)) self.calib_graph.setObjectName("calib_graph") self.gridLayout.addWidget(self.calib_graph, 1, 0, 1, 1) - self.baseline_graph = PlotWidget(self.layoutWidget) + self.baseline_graph = NMRPlotWidget(self.layoutWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -193,7 +193,7 @@ class Ui_Dialog(object): self.baseline_graph.setMinimumSize(QtCore.QSize(300, 200)) self.baseline_graph.setObjectName("baseline_graph") self.gridLayout.addWidget(self.baseline_graph, 0, 1, 1, 1) - self.end_graph = PlotWidget(self.layoutWidget) + self.end_graph = NMRPlotWidget(self.layoutWidget) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -228,4 +228,4 @@ class Ui_Dialog(object): self.cp_checkBox.setText(_translate("Dialog", "Use reference to convert to heat capacity")) self.ref_add_pushButton.setText(_translate("Dialog", "Add reference")) self.ref_remove_pushButton.setText(_translate("Dialog", "Remove reference")) -from pyqtgraph import PlotWidget +from ..lib.graph_items import NMRPlotWidget diff --git a/src/gui_qt/_py/fitdialog.py b/src/gui_qt/_py/fitdialog.py index cf7d7f6..70ae70a 100644 --- a/src/gui_qt/_py/fitdialog.py +++ b/src/gui_qt/_py/fitdialog.py @@ -38,7 +38,6 @@ class Ui_FitDialog(object): self.weight_combobox.addItem("") self.weight_combobox.addItem("") self.weight_combobox.addItem("") - self.weight_combobox.addItem("") self.gridLayout_2.addWidget(self.weight_combobox, 6, 1, 1, 1) self.newmodel_button = QtWidgets.QPushButton(self.scrollAreaWidgetContents_2) self.newmodel_button.setEnabled(False) @@ -144,7 +143,6 @@ class Ui_FitDialog(object): self.weight_combobox.setItemText(1, _translate("FitDialog", "y")) self.weight_combobox.setItemText(2, _translate("FitDialog", "y²")) self.weight_combobox.setItemText(3, _translate("FitDialog", "Δy")) - self.weight_combobox.setItemText(4, _translate("FitDialog", "log(y)")) self.newmodel_button.setText(_translate("FitDialog", "New model")) self.deletemodel_button.setText(_translate("FitDialog", "Delete model")) self.label_3.setText(_translate("FitDialog", "Weight")) diff --git a/src/gui_qt/_py/graph.py b/src/gui_qt/_py/graph.py index 8ecac0b..62feba0 100644 --- a/src/gui_qt/_py/graph.py +++ b/src/gui_qt/_py/graph.py @@ -210,7 +210,7 @@ class Ui_GraphWindow(object): self.checkBox.setChecked(True) self.checkBox.setObjectName("checkBox") self.gridLayout.addWidget(self.checkBox, 0, 1, 1, 1) - self.graphic = PlotWidget(GraphWindow) + self.graphic = NMRPlotWidget(GraphWindow) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -274,5 +274,5 @@ class Ui_GraphWindow(object): self.label_6.setText(_translate("GraphWindow", "X Axis")) self.label_7.setText(_translate("GraphWindow", "Y Axis")) self.checkBox.setText(_translate("GraphWindow", "Show legend")) +from ..lib.graph_items import NMRPlotWidget from ..lib.listwidget import QListWidgetSelect -from pyqtgraph import PlotWidget diff --git a/src/gui_qt/_py/phase_corr_dialog.py b/src/gui_qt/_py/phase_corr_dialog.py index a8c9c51..b0f12fe 100644 --- a/src/gui_qt/_py/phase_corr_dialog.py +++ b/src/gui_qt/_py/phase_corr_dialog.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'resources/_ui/phase_corr_dialog.ui' +# Form implementation generated from reading ui file 'src/resources/_ui/phase_corr_dialog.ui' # -# Created by: PyQt5 UI code generator 5.15.4 +# Created by: PyQt5 UI code generator 5.15.9 # # 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. @@ -24,7 +24,7 @@ class Ui_SignalEdit(object): self.gridLayout.setContentsMargins(6, 6, 6, 6) self.gridLayout.setSpacing(3) self.gridLayout.setObjectName("gridLayout") - self.graphicsView = PlotWidget(SignalEdit) + self.graphicsView = NMRPlotWidget(SignalEdit) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -83,8 +83,8 @@ class Ui_SignalEdit(object): self.gridLayout.addItem(spacerItem1, 1, 5, 1, 1) self.retranslateUi(SignalEdit) - self.buttonBox.accepted.connect(SignalEdit.accept) - self.buttonBox.rejected.connect(SignalEdit.close) + self.buttonBox.accepted.connect(SignalEdit.accept) # type: ignore + self.buttonBox.rejected.connect(SignalEdit.close) # type: ignore QtCore.QMetaObject.connectSlotsByName(SignalEdit) def retranslateUi(self, SignalEdit): @@ -94,4 +94,4 @@ class Ui_SignalEdit(object): self.label_8.setText(_translate("SignalEdit", "Pivot")) self.label_6.setText(_translate("SignalEdit", "Phase 1")) self.label.setText(_translate("SignalEdit", "Phase 0")) -from pyqtgraph import PlotWidget +from ..lib.graph_items import NMRPlotWidget diff --git a/src/gui_qt/_py/shift_scale_dialog.py b/src/gui_qt/_py/shift_scale_dialog.py index 7997c30..fbb01ed 100644 --- a/src/gui_qt/_py/shift_scale_dialog.py +++ b/src/gui_qt/_py/shift_scale_dialog.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'resources/_ui/shift_scale_dialog.ui' +# Form implementation generated from reading ui file 'src/resources/_ui/shift_scale_dialog.ui' # -# Created by: PyQt5 UI code generator 5.12.3 +# Created by: PyQt5 UI code generator 5.15.9 # -# 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 @@ -162,7 +163,7 @@ class Ui_shift_dialog(object): self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.verticalFrame_2) self.verticalLayout_2.setSpacing(3) self.verticalLayout_2.setObjectName("verticalLayout_2") - self.graphicsView = PlotWidget(self.verticalFrame_2) + self.graphicsView = NMRPlotWidget(self.verticalFrame_2) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -267,8 +268,8 @@ class Ui_shift_dialog(object): self.retranslateUi(shift_dialog) self.tabWidget.setCurrentIndex(0) - self.buttonBox.accepted.connect(shift_dialog.accept) - self.buttonBox.rejected.connect(shift_dialog.reject) + self.buttonBox.accepted.connect(shift_dialog.accept) # type: ignore + self.buttonBox.rejected.connect(shift_dialog.reject) # type: ignore QtCore.QMetaObject.connectSlotsByName(shift_dialog) shift_dialog.setTabOrder(self.tabWidget, self.shift_table) shift_dialog.setTabOrder(self.shift_table, self.x_shift_spinbox) @@ -310,5 +311,5 @@ class Ui_shift_dialog(object): self.overwrite_checkbox.setText(_translate("shift_dialog", "Overwrite data")) self.data_newgraph.setText(_translate("shift_dialog", "New graph")) self.values_newgraph.setText(_translate("shift_dialog", "New graph")) +from ..lib.graph_items import NMRPlotWidget from ..lib.spinboxes import SciSpinBox -from pyqtgraph import PlotWidget diff --git a/src/gui_qt/_py/t1dialog.py b/src/gui_qt/_py/t1dialog.py index 7f6c421..f277459 100644 --- a/src/gui_qt/_py/t1dialog.py +++ b/src/gui_qt/_py/t1dialog.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- -# Form implementation generated from reading ui file 'resources/_ui/t1dialog.ui' +# Form implementation generated from reading ui file 'src/resources/_ui/t1dialog.ui' # -# Created by: PyQt5 UI code generator 5.12.3 +# Created by: PyQt5 UI code generator 5.15.9 # -# 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 @@ -160,6 +161,7 @@ class Ui_t1dialog(object): self.tau_combox.addItem("") self.gridLayout_4.addWidget(self.tau_combox, 1, 0, 1, 2) self.checkBox_interpol = QtWidgets.QCheckBox(self.groupBox_3) + self.checkBox_interpol.setEnabled(False) self.checkBox_interpol.setObjectName("checkBox_interpol") self.gridLayout_4.addWidget(self.checkBox_interpol, 2, 0, 1, 2) self.graph_checkbox = QtWidgets.QCheckBox(self.groupBox_3) diff --git a/src/gui_qt/_py/tnmh_dialog.py b/src/gui_qt/_py/tnmh_dialog.py index 7581c52..f683f18 100644 --- a/src/gui_qt/_py/tnmh_dialog.py +++ b/src/gui_qt/_py/tnmh_dialog.py @@ -59,7 +59,7 @@ class Ui_DSCEvalDialog(object): self.tg_tree.headerItem().setText(0, "1") self.tg_tree.header().setVisible(False) self.gridLayout_2.addWidget(self.tg_tree, 2, 0, 1, 1) - self.dsc_plot = PlotWidget(self.page) + self.dsc_plot = NMRPlotWidget(self.page) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -89,10 +89,10 @@ class Ui_DSCEvalDialog(object): self.label_4 = QtWidgets.QLabel(self.page_2) self.label_4.setObjectName("label_4") self.gridLayout_3.addWidget(self.label_4, 2, 0, 1, 1) - self.tghodge_graph = PlotWidget(self.page_2) + self.tghodge_graph = NMRPlotWidget(self.page_2) self.tghodge_graph.setObjectName("tghodge_graph") self.gridLayout_3.addWidget(self.tghodge_graph, 1, 0, 1, 1) - self.tau_plot = PlotWidget(self.page_2) + self.tau_plot = NMRPlotWidget(self.page_2) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.MinimumExpanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) @@ -155,7 +155,7 @@ class Ui_DSCEvalDialog(object): self.page_3.setObjectName("page_3") self.gridLayout_6 = QtWidgets.QGridLayout(self.page_3) self.gridLayout_6.setObjectName("gridLayout_6") - self.tnmh_graphics = PlotWidget(self.page_3) + self.tnmh_graphics = NMRPlotWidget(self.page_3) self.tnmh_graphics.setObjectName("tnmh_graphics") self.gridLayout_6.addWidget(self.tnmh_graphics, 1, 0, 1, 2) self.tnmh_tree = QtWidgets.QTreeWidget(self.page_3) @@ -249,5 +249,5 @@ class Ui_DSCEvalDialog(object): self.back_button.setText(_translate("DSCEvalDialog", "Back")) self.next_button.setText(_translate("DSCEvalDialog", "Next")) self.close_button.setText(_translate("DSCEvalDialog", "Close")) +from ..lib.graph_items import NMRPlotWidget from ..lib.listwidget import QListWidgetSelect -from pyqtgraph import PlotWidget diff --git a/src/gui_qt/data/container.py b/src/gui_qt/data/container.py index 82dabc6..913ad7e 100644 --- a/src/gui_qt/data/container.py +++ b/src/gui_qt/data/container.py @@ -463,6 +463,16 @@ class ExperimentContainer(QtCore.QObject): return offset + @plot_update + def shift_scale(self, shift_factor: tuple[float, float], scaling_factor: tuple[float, float]): + scale_x, scale_y = scaling_factor + shift_x, shift_y = shift_factor + self.data.x = self.data.x * scale_x + shift_x + self.data.y = self.data.y * scale_y + shift_y + self.data.y_err = self.data.y_err * scale_y + + self.update({'shift': scaling_factor, 'scale': shift_factor}) + def get_namespace(self, i: int = None, j: int = None) -> dict: if (i is None) and (j is None): prefix = '' diff --git a/src/gui_qt/data/shift_graphs.py b/src/gui_qt/data/shift_graphs.py index 61dc918..f436989 100644 --- a/src/gui_qt/data/shift_graphs.py +++ b/src/gui_qt/data/shift_graphs.py @@ -1,7 +1,7 @@ import numpy as np from itertools import cycle -from pyqtgraph import mkColor, mkPen +from pyqtgraph import mkColor, mkPen, mkBrush from nmreval.lib.colors import Tab10 @@ -42,11 +42,17 @@ class QShift(QtWidgets.QDialog, Ui_shift_dialog): def add_item(self, idx, name, x, y): color = mkColor(next(self._colors).rgb()) - if np.iscomplexobj(y): - pl = [PlotItem(x=x, y=y.real, name=name, pen=mkPen(color=color)), - PlotItem(x=x, y=y.imag, name=name, pen=mkPen(color=color))] + + if len(y) == 1: + sym_kwds = {'symbol': 'o', 'symbolBrush': mkBrush(color=color), 'symbolPen': mkPen(color=color)} else: - pl = [PlotItem(x=x, y=y, name=name, pen=mkPen(color=color))] + sym_kwds = {'symbol': None, 'symbolBrush': mkBrush(color=color), 'symbolPen': mkPen(color=color)} + + if np.iscomplexobj(y): + pl = [PlotItem(x=x, y=y.real, name=name, pen=mkPen(color=color), **sym_kwds), + PlotItem(x=x, y=y.imag, name=name, pen=mkPen(color=color), **sym_kwds)] + else: + pl = [PlotItem(x=x, y=y, name=name, pen=mkPen(color=color), **sym_kwds)] self.data[idx] = (pl, x, y) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index 0a4179f..2d5f85c 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -6,12 +6,14 @@ 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): 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) @@ -30,6 +32,7 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): 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) @@ -311,7 +314,7 @@ class FitModelTree(QtWidgets.QTreeWidget): return funcs -class FitTableWidget(QtWidgets.QTableWidget): +class FitTableWidget(TableWidget): def __init__(self, parent=None): super().__init__(parent=parent) diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index ae41510..f4894f0 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -78,6 +78,7 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): widgt.state_changed.connect(self.make_global) widgt.value_requested.connect(self.look_for_value) widgt.value_changed.connect(self.change_global_parameter) + widgt.replace_single_value.connect(self.delete_single_parameter) self.global_parameter.append(widgt) self.scrollwidget.layout().addWidget(widgt) @@ -150,6 +151,13 @@ class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit): if value is None: self.change_data(self.comboBox.currentIndex()) + def delete_single_parameter(self): + idx = self.global_parameter.index(self.sender()) + for i in range(self.comboBox.count()): + set_id = self.comboBox.itemData(i) + self.data_values[set_id][idx] = None + self.change_data(self.comboBox.currentIndex()) + def change_single_choice(self, _, value, sender=None): if sender is None: sender = self.sender() @@ -298,7 +306,8 @@ class ParameterSingleWidget(QtWidgets.QWidget): self._name = name self.label.setText(convert(name)) - self.label.setToolTip('IIf this is bold then this parameter is only for this data. otherwise the general parameter is used and displayed') + self.label.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()) self.value_line.textChanged.connect(lambda: self.valueChanged.emit(self.value) if self.value is not None else 0) diff --git a/src/gui_qt/fit/function_creation_dialog.py b/src/gui_qt/fit/function_creation_dialog.py index b63ee29..41d9363 100644 --- a/src/gui_qt/fit/function_creation_dialog.py +++ b/src/gui_qt/fit/function_creation_dialog.py @@ -14,7 +14,7 @@ from gui_qt.lib.namespace import QNamespaceWidget __all__ = ['QUserFitCreator'] -validator = QtGui.QRegExpValidator(QtCore.QRegExp('[A-Za-z]\S*')) +validator = QtGui.QRegExpValidator(QtCore.QRegExp('[_A-Za-z][_A-Za-z0-9]*')) pattern = re.compile(r'def func\(.*\):', flags=re.MULTILINE) @@ -145,6 +145,7 @@ class QUserFitCreator(QtWidgets.QDialog, Ui_Dialog): self.classCreated.emit() super().accept() + class KwargsWidget(QtWidgets.QWidget): Changed = QtCore.pyqtSignal() @@ -209,7 +210,7 @@ class KwargsWidget(QtWidgets.QWidget): def get_strings(self) -> str: kwargs = [] if self.use_nuclei.isChecked(): - kwargs.append("(r'\gamma', 'nucleus', gamma)") + kwargs.append(r"(r'\gamma', 'nucleus', gamma)") for i in range(self.choices.count()): kwargs.append(self.choices.widget(i).get_strings()) @@ -300,7 +301,7 @@ class ChoiceWidget(QtWidgets.QWidget): def get_strings(self) -> str: opts = [] for i in range(self.table.rowCount()): - name = self.table.item(i, 0).text() + name = self.table.cellWidget(i, 0).text() val = self._make_value(i) opts.append(f'{name!r}: {val!r}') diff --git a/src/gui_qt/fit/result.py b/src/gui_qt/fit/result.py index 6a820ed..a7e77f7 100644 --- a/src/gui_qt/fit/result.py +++ b/src/gui_qt/fit/result.py @@ -3,6 +3,7 @@ from math import isnan from pyqtgraph import mkBrush, mkPen from nmreval.utils.text import convert +from ..lib.graph_items import logTickValues from ..lib.utils import RdBuCMap from ..Qt import QtWidgets, QtGui, QtCore @@ -33,8 +34,12 @@ class QFitResult(QtWidgets.QDialog, Ui_Dialog): self.graph_opts = {} self.last_idx = None - self.resid_plot = self.graphicsView.addPlot(row=0, col=0, title='Residual') self.fit_plot = self.graphicsView.addPlot(row=1, col=0, title='Fit') + self.resid_plot = self.graphicsView.addPlot(row=0, col=0, title='Residual') + + for orient in ['top', 'bottom', 'left', 'right']: + self.fit_plot.getAxis(orient).logTickValues = logTickValues + self.resid_plot.getAxis(orient).logTickValues = logTickValues self.graphicsView.ci.layout.setRowStretchFactor(0, 1) self.graphicsView.ci.layout.setRowStretchFactor(1, 2) diff --git a/src/gui_qt/graphs/graphwindow.py b/src/gui_qt/graphs/graphwindow.py index 134a6cc..ce40d71 100644 --- a/src/gui_qt/graphs/graphwindow.py +++ b/src/gui_qt/graphs/graphwindow.py @@ -4,7 +4,7 @@ import itertools import os import uuid -from math import isnan +from math import isfinite from pathlib import Path import numpy as np @@ -45,7 +45,7 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): self.id = str(uuid.uuid4()) self.sets = [] - self.active = [] + self._active = [] self.real_plots = {} self.imag_plots = {} @@ -75,6 +75,8 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): self.scene.contextMenu[0].disconnect() self.scene.contextMenu[0].triggered.connect(self.export_dialog) + self.bwbutton.toggled.connect(self.change_background) + def _init_gui(self): self.setWindowTitle('Graph ' + str(next(QGraphWindow.counter))) @@ -114,11 +116,11 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): return iter(self.active) def __len__(self): - return len(self.active) + return len(self._active) def curves(self) -> tuple: for set_id in self.sets: - if set_id in self.active: + if set_id in self._active: if self.real_button.isChecked(): if self.error_plots[set_id] is not None: yield self.real_plots[set_id], self.error_plots[set_id] @@ -144,7 +146,7 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): tmp = [np.nan, np.nan] for j, x in enumerate(r[i]): try: - tmp[j] = 10**x + tmp[j] = 10**min(x, 199) except OverflowError: pass r[i] = tuple(tmp) @@ -153,6 +155,14 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): return tuple(r) + @property + def active(self) -> list: + return [set_id for set_id in self.sets if set_id in self._active] + + @active.setter + def active(self, value: list): + self._active = value + def block(self, state: bool): self._block = state @@ -201,8 +211,8 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): for plot in [self.real_plots, self.imag_plots, self.error_plots]: self.graphic.removeItem(plot[n]) - if n in self.active: - self.active.remove(n) + if n in self._active: + self._active.remove(n) # remove from label list self.listWidget.blockSignals(True) @@ -246,8 +256,8 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): return for a in idlist: - if a not in self.active: - self.active.append(a) + if a not in self._active: + self._active.append(a) for (bttn, plot_dic) in [ (self.real_button, self.real_plots), @@ -266,8 +276,8 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): return for r in idlist: - if r in self.active: - self.active.remove(r) + if r in self._active: + self._active.remove(r) for plt in [self.real_plots, self.imag_plots, self.error_plots]: item = plt[r] @@ -289,7 +299,7 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): else: func = self.graphic.removeItem - for a in self.active: + for a in self._active: item = plots[a] if item is not None: func(item) @@ -306,12 +316,12 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): return if visible: - for a in self.active: + for a in self._active: item = self.error_plots[a] if (item is not None) and (item not in self.graphic.items()): self.graphic.addItem(item) else: - for a in self.active: + for a in self._active: item = self.error_plots[a] if (item is not None) and (item in self.graphic.items()): self.graphic.removeItem(item) @@ -423,6 +433,9 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): def set_logmode(self, xmode: bool = None, ymode: bool = None): r = self.ranges + self.plotItem.setXRange(*r[0]) + self.plotItem.setYRange(*r[1]) + if xmode is None: xmode = self.plotItem.ctrl.logXCheck.isChecked() else: @@ -439,6 +452,7 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): item.logmode[0] = self.log[:] self.plotItem.updateLogMode() + self.set_range(x=r[0], y=r[1]) self.plotItem.enableAutoRange() @@ -481,9 +495,9 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): with errstate(all='ignore'): xy = [log10(val) for val in xy] - if isnan(xy[1]): + if not isfinite(xy[1]): xy = [-1, 1] - elif isnan(xy[0]): + elif not isfinite(xy[0]): xy[0] = xy[1]-4 func(xy[0], xy[1], padding=0) @@ -669,7 +683,7 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): 'legend': self.legend.isVisible(), 'plots': (self.real_button.isChecked(), self.imag_button.isChecked(), self.error_button.isChecked()), 'children': self.sets, - 'active': self.active, + 'active': self._active, } in_legend = [] @@ -760,8 +774,8 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): if y is not None: self.plotItem.setLabel('left', y, **{'font-size': '10pt', 'color': self._fgcolor.name()}) - @QtCore.pyqtSlot(bool, name='on_bwbutton_toggled') def change_background(self, _): temp = self._fgcolor, self._bgcolor self.set_color(foreground=self._prev_colors[0], background=self._prev_colors[1]) self._prev_colors = temp + diff --git a/src/gui_qt/io/gracereader.py b/src/gui_qt/io/gracereader.py index be3d62a..ca1e414 100644 --- a/src/gui_qt/io/gracereader.py +++ b/src/gui_qt/io/gracereader.py @@ -4,6 +4,7 @@ from nmreval.lib.lines import LineStyle from nmreval.lib.symbols import SymbolStyle from nmreval.data.points import Points from nmreval.io.graceeditor import GraceEditor +from nmreval.utils.text import convert from ..Qt import QtCore, QtWidgets, QtGui from .._py.gracereader import Ui_Dialog @@ -55,7 +56,7 @@ class QGraceReader(QtWidgets.QDialog, Ui_Dialog): if ds is None: continue - item_2 = QtWidgets.QTreeWidgetItem([f'Set {gset.idx} (Label: {gset.get_property("legend")}, ' + item_2 = QtWidgets.QTreeWidgetItem([f'Set {gset.idx} (Label: {convert(gset.get_property("legend"), old="agr", new="str")}, ' f'shape: {ds.shape})']) item_2.setCheckState(0, QtCore.Qt.Checked) item_2.setData(0, QtCore.Qt.UserRole, (graphs.idx, gset.idx)) @@ -94,6 +95,7 @@ class QGraceReader(QtWidgets.QDialog, Ui_Dialog): label = '' else: label = label.replace('"', '') + label = convert(label, old='agr', new='str') sd = s.data sd = np.atleast_2d(sd) if s.type == 'xydy': diff --git a/src/gui_qt/lib/codeeditor.py b/src/gui_qt/lib/codeeditor.py index 245a9e1..1b80d4d 100644 --- a/src/gui_qt/lib/codeeditor.py +++ b/src/gui_qt/lib/codeeditor.py @@ -237,7 +237,7 @@ class CodeEditor(QtWidgets.QPlainTextEdit): if block.isVisible() and (bottom >= evt.rect().top()): number = str(block_number + 1) painter.setPen(QtCore.Qt.black) - painter.drawText(0, top, self.current_linenumber.width() - 3, height, + painter.drawText(0, int(top), self.current_linenumber.width() - 3, height, QtCore.Qt.AlignRight, number) block = block.next() diff --git a/src/gui_qt/lib/forms.py b/src/gui_qt/lib/forms.py index c0b73aa..ab20ce3 100644 --- a/src/gui_qt/lib/forms.py +++ b/src/gui_qt/lib/forms.py @@ -52,19 +52,24 @@ class QDelayWidget(QtWidgets.QWidget): class LineEdit(QtWidgets.QLineEdit): values_requested = QtCore.pyqtSignal() + replace_single_values = QtCore.pyqtSignal() def __init__(self, parent=None): super().__init__(parent=parent) def contextMenuEvent(self, evt): menu = self.createStandardContextMenu() - request_action = menu.addAction('Use value of sets') + request_action = menu.addAction('Use numeric value of sets') + set_value_action = menu.addAction('Replace single set values') action = menu.exec(evt.globalPos()) if action == request_action: self.values_requested.emit() + elif action == set_value_action: + self.replace_single_values.emit() + class LineEditPost(QtWidgets.QLineEdit): values_requested = QtCore.pyqtSignal() diff --git a/src/gui_qt/lib/graph_items.py b/src/gui_qt/lib/graph_items.py new file mode 100644 index 0000000..85e826c --- /dev/null +++ b/src/gui_qt/lib/graph_items.py @@ -0,0 +1,53 @@ +from numpy import log10, arange, floor, ceil +from pyqtgraph import PlotWidget, PlotItem + + +__all__ = ['NMRPlotWidget', 'logTickValues'] + + +class NMRPlotWidget(PlotWidget): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + for orient in ['top', 'bottom', 'left', 'right']: + # BAD HACK!!! but seems to work, see function for explanation + self.plotItem.getAxis(orient).logTickValues = logTickValues + + +class NMRPlotItem(PlotItem): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + for orient in ['top', 'bottom', 'left', 'right']: + self.plotItem.getAxis(orient).logTickValues = logTickValues + + +def logTickValues(minVal, maxVal, size, stdTicks): + # TODO FIND A BETTER SOLUTION!!! + # Sometimes minVal and maxVal are not log-scaled values and the loop from v1 to v2 is humongous, + # The minor list then fills the RAM completely and freezes everything + # Until there is a better solution, we overwrite this function for every AxesItem + # and do not draw minor ticks at all if there are too many + + # start with the tick spacing given by tickValues(). + # Any level whose spacing is < 1 needs to be converted to log scale + ticks = [] + for (spacing, t) in stdTicks: + if spacing >= 1.0: + ticks.append((spacing, t)) + + if len(ticks) < 3: + v1 = int(floor(minVal)) + v2 = int(ceil(maxVal)) + # major = list(range(v1+1, v2)) + minor = [] + + if v2 - v1 < 400: + for v in range(v1, v2): + minor.extend(v + log10(arange(1, 10))) + minor = [x for x in minor if minVal < x < maxVal] + ticks.append((None, minor)) + return ticks + diff --git a/src/gui_qt/lib/pg_objects.py b/src/gui_qt/lib/pg_objects.py index 9978031..923ca88 100644 --- a/src/gui_qt/lib/pg_objects.py +++ b/src/gui_qt/lib/pg_objects.py @@ -375,9 +375,6 @@ class PlotItem(PlotDataItem): class RegionItem(LinearRegionItem): def __init__(self, *args, **kwargs): self.mode = kwargs.pop('mode', 'half') - - print(args, kwargs) - super().__init__(*args, **kwargs) self.logmode = False @@ -475,11 +472,18 @@ class LegendItemBlock(LegendItem): def mouseDragEvent(self, ev): if ev.button() == QtCore.Qt.LeftButton: ev.accept() + dpos = ev.pos() - ev.lastPos() + + upper_left = self.pos() + lower_right = self.pos() + lower_right.setX(lower_right.x() + self.width()) + lower_right.setY(lower_right.y() + self.height()) + vb_rect = self.parentItem().rect() - pos = self.pos() - # upper left corner and a point a little more to the bottom right must be inside - if vb_rect.contains(pos+dpos) and vb_rect.contains(pos+dpos+QtCore.QPointF(20., 20.)): - self.autoAnchor(pos + dpos) + + # upper left and lower right corner must be inside viewbox + if vb_rect.contains(upper_left + dpos) and vb_rect.contains(lower_right + dpos): + self.autoAnchor(upper_left + dpos) else: - self.autoAnchor(pos) + self.autoAnchor(upper_left) diff --git a/src/gui_qt/lib/tables.py b/src/gui_qt/lib/tables.py index 15195c4..a7b6ef1 100644 --- a/src/gui_qt/lib/tables.py +++ b/src/gui_qt/lib/tables.py @@ -28,4 +28,17 @@ class TreeWidget(QtWidgets.QTreeWidget): continue it.setCheckState(0, QtCore.Qt.Unchecked if it.checkState(0) == QtCore.Qt.Checked else QtCore.Qt.Checked) else: - super().keyPressEvent(evt) \ No newline at end of file + super().keyPressEvent(evt) + + +class TableWidget(QtWidgets.QTableWidget): + def keyPressEvent(self, evt: QtGui.QKeyEvent): + if evt.key() == QtCore.Qt.Key.Key_Space: + for idx in self.selectedIndexes(): + item = self.itemFromIndex(idx) + cs = item.checkState() + item.setCheckState(QtCore.Qt.CheckState.Unchecked if cs == QtCore.Qt.CheckState.Checked + else QtCore.Qt.CheckState.Checked) + else: + super().keyPressEvent(evt) + diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 6973141..6f1c47b 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -211,7 +211,6 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.ptsselectwidget.points_selected.connect(self.management.extract_points) - self.t1tauwidget.newData.connect(self.management.add_new_data) self.t1tauwidget.newData.connect(self.management.add_new_data) self.editsignalwidget.do_something.connect(self.management.apply) @@ -917,10 +916,12 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.action_odr_fit: 'odr' }[self.ac_group.checkedAction()] - self.fit_dialog.fit_button.setEnabled(False) - self.management.start_fit(parameter, links, fit_options) - self.status.setText('Fit running...'.format(self.management.fitter.step)) - self.fit_timer.start(500) + fit_is_ready = self.management.prepare_fit(parameter, links, fit_options) + if fit_is_ready: + self.management.start_fit() + self.fit_dialog.fit_button.setEnabled(False) + self.status.setText('Fit running...'.format(self.management.fitter.step)) + self.fit_timer.start(500) @QtCore.pyqtSlot(dict, int, bool) def show_fit_preview(self, funcs: dict, num: int, show: bool): diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index dfc98d7..e4a0dc4 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -58,11 +58,18 @@ class GraphDict(OrderedDict): def list(self): return [(k, v.title) for k, v in self.items()] - def active(self, key: str): - if key: - return [(self._data[i].id, self._data[i].name) for i in self[key]] - else: + def active(self, key: str, return_val: str = 'both'): + if not key: return [] + else: + if return_val == 'both': + return [(self._data[i].id, self._data[i].name) for i in self[key]] + elif return_val == 'id': + return [self._data[i].id for i in self[key]] + elif return_val == 'name': + return [self._data[i].name for i in self[key]] + else: + raise ValueError(f'return_val got wrong value {return_val!r}') def current_sets(self, key: str): if key: @@ -148,6 +155,10 @@ class UpperManagement(QtCore.QObject): def active_sets(self): return self.graphs.active(self.current_graph) + @property + def active_id(self): + return self.graphs.active(self.current_graph, return_val='id') + def get_attributes(self, graph_id: str, attr: str) -> dict[str, Any]: return {self.data[i].id: getattr(self.data[i], attr) for i in self.graphs[graph_id].sets} @@ -413,9 +424,9 @@ class UpperManagement(QtCore.QObject): for d in self.data.values(): d.mask = np.ones_like(d.mask, dtype=bool) - def start_fit(self, parameter: dict, links: list, fit_options: dict): + def prepare_fit(self, parameter: dict, links: list, fit_options: dict) -> bool: if self._fit_active: - return + return False self.__fit_options = (parameter, links, fit_options) @@ -423,60 +434,84 @@ class UpperManagement(QtCore.QObject): models = {} fit_limits = fit_options['limits'] fit_mode = fit_options['fit_mode'] - we = fit_options['we'] + we_option = fit_options['we'] - for model_id, model_p in parameter.items(): - m = Model(model_p['func']) - models[model_id] = m + self.fitter.fitmethod = fit_mode - m_complex = model_p['complex'] + # all-encompassing error catch + try: + for model_id, model_p in parameter.items(): + m = Model(model_p['func']) + models[model_id] = m - for set_id, set_params in model_p['parameter'].items(): - data_i = self.data[set_id] - if we.lower() == 'deltay': - we = data_i.y_err**2 + m_complex = model_p['complex'] - if m_complex is None or m_complex == 1: - _y = data_i.y.real - elif m_complex == 2 and np.iscomplexobj(data_i.y): - _y = data_i.y.imag - else: - _y = data_i.y + # sets are not in active order but in order they first appeared in fit dialog + # iterate over order of set id in active order and access parameter inside loop + # instead of directly looping + list_ids = list(model_p['parameter'].keys()) + set_order = [self.active_id.index(i) for i in list_ids] + for pos in set_order: + set_id = list_ids[pos] - _x = data_i.x + data_i = self.data[set_id] + set_params = model_p['parameter'][set_id] - 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])) - else: - inside = np.where((_x >= fit_limits[0]) & (_x <= fit_limits[1])) + if we_option.lower() == 'deltay': + we = data_i.y_err**2 + else: + we = we_option - if isinstance(we, str): - d = fit_d.Data(_x[inside], _y[inside], we=we, idx=set_id) - else: - d = fit_d.Data(_x[inside], _y[inside], we=we[inside], idx=set_id) + if m_complex is None or m_complex == 1: + _y = data_i.y.real + elif m_complex == 2 and np.iscomplexobj(data_i.y): + _y = data_i.y.imag + else: + _y = data_i.y - d.set_model(m) - d.set_parameter(set_params[0], var=model_p['var'], - lb=model_p['lb'], ub=model_p['ub'], - fun_kwargs=set_params[1]) + _x = data_i.x - self.fitter.add_data(d) + 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])) + else: + inside = np.where((_x >= fit_limits[0]) & (_x <= fit_limits[1])) - model_globs = model_p['glob'] - if model_globs: - for parameter_args in zip(*model_globs.values()): - m.set_global_parameter(**{k: v for k, v in zip(model_globs.keys(), parameter_args)}) - # m.set_global_parameter(**model_p['glob']) + if isinstance(we, str): + d = fit_d.Data(_x[inside], _y[inside], we=we, idx=set_id) + else: + d = fit_d.Data(_x[inside], _y[inside], we=we[inside], idx=set_id) - for links_i in links: - self.fitter.set_link_parameter((models[links_i[0]], links_i[1]), - (models[links_i[2]], links_i[3])) + d.set_model(m) + d.set_parameter(set_params[0], var=model_p['var'], + lb=model_p['lb'], ub=model_p['ub'], + fun_kwargs=set_params[1]) + self.fitter.add_data(d) + + model_globs = model_p['glob'] + if model_globs: + for parameter_args in zip(*model_globs.values()): + m.set_global_parameter(**{k: v for k, v in zip(model_globs.keys(), parameter_args)}) + # m.set_global_parameter(**model_p['glob']) + + for links_i in links: + self.fitter.set_link_parameter((models[links_i[0]], links_i[1]), + (models[links_i[2]], links_i[3])) + return True + + except Exception as e: + logger.error('Fit preparation failed', *e.args) + QtWidgets.QMessageBox.warning(QtWidgets.QWidget(), + 'Fit prep failed', + f'Fit preparation failed with message\n{e.args}') + return False + + def start_fit(self): with busy_cursor(): - self.fit_worker = FitWorker(self.fitter, fit_mode) + self.fit_worker = FitWorker(self.fitter) self.fit_thread = QtCore.QThread() self.fit_worker.moveToThread(self.fit_thread) @@ -512,7 +547,8 @@ class UpperManagement(QtCore.QObject): for set_id, set_parameter in parameter.items(): new_values = [v.value for v in res[set_id].parameter.values()] parameter[set_id] = (new_values, set_parameter[1]) - self.start_fit(*self.__fit_options) + if self.prepare_fit(*self.__fit_options): + self.start_fit() def make_fits(self, res: dict, opts: list, param_graph: str, show_fit: bool, parts: bool, extrapolate: list) -> None: """ @@ -637,7 +673,7 @@ class UpperManagement(QtCore.QObject): def save_fit_parameter(self, fname: str | pathlib.Path, fit_sets: list[str] = None): if fit_sets is None: - fit_sets = [s for (s, _) in self.active_sets] + fit_sets = [s for s in self.active_id] for set_id in fit_sets: data = self.data[set_id] @@ -843,13 +879,10 @@ class UpperManagement(QtCore.QObject): d_k = self.data[k] if copy_data is None: - d_k.x = d_k.x*v[1][0] + v[0][0] - d_k.y = d_k.y*v[1][1] + v[0][1] + d_k.shift_scale(v[0], v[1]) else: new_data = d_k.copy(full=True) - new_data.update({'shift': v[0], 'scale': v[1]}) - new_data.data.x = new_data.x*v[1][0] + v[0][0] - new_data.y = new_data.y*v[1][1] + v[0][1] + new_data.shift_scale(v[0], v[1]) sid = self.add(new_data) sid_list.append(sid) @@ -1009,7 +1042,7 @@ class UpperManagement(QtCore.QObject): def show_statistics(self, mode): x, y, = [], [] - for i, _ in self.active_sets: + for i in self.active_id: _temp = self.data[i] try: x.append(float(_temp.name)) @@ -1020,7 +1053,7 @@ class UpperManagement(QtCore.QObject): @QtCore.pyqtSlot() def calc_magn(self): new_id = [] - for k, _ in self.active_sets: + for k in self.active_id: dataset = self.data[k] if isinstance(dataset, SignalContainer): new_value = dataset.copy(full=True) @@ -1032,7 +1065,7 @@ class UpperManagement(QtCore.QObject): @QtCore.pyqtSlot() def center(self): new_id = [] - for k, _ in self.active_sets: + for k in self.active_id: new_value = self.data[k].copy(full=True) new_value.x -= new_value.x[np.argmax(new_value.y.real)] new_id.append(self.add(new_value)) @@ -1071,7 +1104,7 @@ class UpperManagement(QtCore.QObject): def bds_deriv(self): new_sets = [] - for (set_id, _) in self.active_sets: + for set_id in self.active_id: data_i = self.data[set_id] diff = data_i.data.diff(log=True) new_data = Points(x=diff.x, y=-np.pi/2*diff.y.real) @@ -1098,7 +1131,7 @@ class UpperManagement(QtCore.QObject): self.newData.emit(new_sets, kwargs['graph']) def skip_points(self, offset: int, step: int, invert: bool = False, copy: bool = False): - for k, _ in self.active_sets: + for k in self.active_id: src = self.data[k] if invert: mask = np.mod(np.arange(offset, src.x.size+offset), step) != 0 @@ -1253,16 +1286,15 @@ class UpperManagement(QtCore.QObject): class FitWorker(QtCore.QObject): finished = QtCore.pyqtSignal(list, bool) - def __init__(self, fitter, mode): + def __init__(self, fitter): super().__init__() self.fitter = fitter - self.mode = mode @QtCore.pyqtSlot() def run(self): try: - res = self.fitter.run(mode=self.mode) + res = self.fitter.run() success = True except Exception as e: res = [e] diff --git a/src/gui_qt/nmr/t1_from_tau.py b/src/gui_qt/nmr/t1_from_tau.py index 00e1f18..261e0aa 100644 --- a/src/gui_qt/nmr/t1_from_tau.py +++ b/src/gui_qt/nmr/t1_from_tau.py @@ -19,7 +19,7 @@ class QRelaxCalc(QtWidgets.QDialog, Ui_Dialog): self.graphs = {} - self.specdens = [ColeCole, ColeDavidson, HavriliakNegami, KWW] + self.specdens = [ColeCole, ColeDavidson, HavriliakNegami, KWW, LogGaussian] self.coupling = [Quadrupolar, HomoDipolar, Czjzek] self.tau_parameter = [] @@ -199,3 +199,9 @@ class QRelaxCalc(QtWidgets.QDialog, Ui_Dialog): def accept(self): self.calc_relaxation() super().accept() + + @QtCore.pyqtSlot(QtWidgets.QAbstractButton) + def on_buttonBox_clicked(self, button: QtWidgets.QAbstractButton): + role = self.buttonBox.buttonRole(button) + if role == self.buttonBox.ApplyRole: + self.calc_relaxation() diff --git a/src/gui_qt/nmr/t1widget.py b/src/gui_qt/nmr/t1widget.py index 2521224..a4c9228 100644 --- a/src/gui_qt/nmr/t1widget.py +++ b/src/gui_qt/nmr/t1widget.py @@ -61,6 +61,8 @@ class QT1Widget(QtWidgets.QDialog, Ui_t1dialog): self.freq_combox.currentIndexChanged.connect(lambda x: self.update_model()) self.freq_spinbox.valueChanged.connect(lambda x: self.update_model()) + self.checkBox_interpol.setVisible(False) + self.update_specdens(0) self.update_coupling(0) diff --git a/src/nmreval/distributions/colecole.py b/src/nmreval/distributions/colecole.py index b8551a8..b1a5a66 100644 --- a/src/nmreval/distributions/colecole.py +++ b/src/nmreval/distributions/colecole.py @@ -54,6 +54,9 @@ class ColeCole(Distribution): tau (array_like): alpha (float): """ + if alpha == 1: + return tau / (1 + omega**2 * tau**2) + omtau = (omega*tau)**alpha return np.sin(alpha*np.pi/2) * omtau / (1 + omtau**2 + 2*np.cos(alpha*np.pi/2)*omtau) / omega diff --git a/src/nmreval/distributions/loggaussian.py b/src/nmreval/distributions/loggaussian.py index 10c3615..8979750 100644 --- a/src/nmreval/distributions/loggaussian.py +++ b/src/nmreval/distributions/loggaussian.py @@ -5,6 +5,7 @@ from typing import Callable import numpy as np from scipy import LowLevelCallable +from scipy.special import erf from nmreval.lib.utils import ArrayLike @@ -32,7 +33,7 @@ class LogGaussian(Distribution): return np.exp(-0.5*(np.log(tau/tau0)/sigma)**2)/np.sqrt(2*np.pi)/sigma @staticmethod - def correlation(t, tau0, sigma: float): + def correlation(t: ArrayLike, tau0: ArrayLike, sigma: float): _t = np.atleast_1d(t) _tau = np.atleast_1d(tau0) @@ -44,7 +45,7 @@ class LogGaussian(Distribution): return res.squeeze() @staticmethod - def susceptibility(omega, tau0, sigma: float): + def susceptibility(omega: ArrayLike, tau0: ArrayLike, sigma: float): _omega = np.atleast_1d(omega) _tau = np.atleast_1d(tau0) @@ -68,6 +69,7 @@ class LogGaussian(Distribution): ret_val = _integration_parallel(_omega, _tau, sigma, _integrate_process_imag) ret_val /= _omega[:, None] + ret_val[_omega == 0, :] = tau[None, :] * np.exp(sigma**2 / 2) return ret_val.squeeze() @@ -113,18 +115,16 @@ def _integrate_susc_c(lowfunc, highfunc, omega, tau, sigma): return res -def _integrate_process_imag(args): - omega_i, tau_j, sigma = args - area = quad(_integrand_freq_imag_high, 0, 50, args=(omega_i, tau_j, sigma), epsabs=1e-12, epsrel=1e-12)[0] - area += quad(_integrand_freq_imag_low, -50, 0, args=(omega_i, tau_j, sigma), epsabs=1e-12, epsrel=1e-12)[0] +def _integrate_process_imag(omega, tau, sigma): + area = quad(_integrand_freq_imag_high, 0, 50, args=(omega, tau, sigma), epsabs=1e-12, epsrel=1e-12)[0] + area += quad(_integrand_freq_imag_low, -50, 0, args=(omega, tau, sigma), epsabs=1e-12, epsrel=1e-12)[0] return area -def _integrate_process_real(args): - omega_i, tau_j, sigma = args - area = quad(_integrand_freq_real_high, 0, 50, args=(omega_i, tau_j, sigma))[0] - area += quad(_integrand_freq_real_low, -50, 0, args=(omega_i, tau_j, sigma))[0] +def _integrate_process_real(omega: float, tau: float, sigma: float): + area = quad(_integrand_freq_real_high, 0, 50, args=(omega, tau, sigma))[0] + area += quad(_integrand_freq_real_low, -50, 0, args=(omega, tau, sigma))[0] return area @@ -145,9 +145,8 @@ def _integrate_correlation_c(t, tau, sigma): return res -def _integrate_process_time(args): - omega_i, tau_j, sigma = args - return quad(_integrand_time, -50, 50, args=(omega_i, tau_j, sigma), epsabs=1e-12, epsrel=1e-12)[0] +def _integrate_process_time(omega, tau, sigma): + return quad(_integrand_time, -50, 50, args=(omega, tau, sigma), epsabs=1e-12, epsrel=1e-12)[0] def _integrand_time(u, t, tau, sigma): diff --git a/src/nmreval/fit/_meta.py b/src/nmreval/fit/_meta.py index 9fe9c34..889cc57 100644 --- a/src/nmreval/fit/_meta.py +++ b/src/nmreval/fit/_meta.py @@ -24,9 +24,10 @@ class ModelFactory: param_len.append(len(func['func'].params)) if func['children']: - right, _, _ = ModelFactory.create_from_list(func['children'], left=func['func'], left_cnt=func['pos'], + 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) else: right = func['func'] right_cnt = func['cnt'] @@ -46,7 +47,13 @@ class MultiModel: 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, right: Any, op: str | Callable | int = '+', left_idx=0, right_idx=1): + def __init__(self, + left: Any, + right: Any, + op: str | Callable | int = '+', + left_idx: int | None = 0, + right_idx: int | None = 1, + ): self._left = left self._right = right diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index bc07df9..50fa4e4 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -82,7 +82,7 @@ def _update_parameter(data: Data, varied_keys: list[str], parameter: list[float] class FitRoutine(object): def __init__(self, mode='lsq'): - self._fitmethod = mode + self.fitmethod = mode self.data = [] self.fit_model = None self._no_own_model = [] @@ -220,9 +220,12 @@ class FitRoutine(object): logger.info('Fit aborted by user') self._abort = True - def run(self, mode: str = 'lsq'): + def run(self, mode: str=None): self._abort = False + if mode is None: + mode = self.fitmethod + fit_groups, linked_parameter = self.prepare_links() for data_groups in fit_groups: if len(data_groups) == 1 and not self.linked: diff --git a/src/nmreval/io/asciireader.py b/src/nmreval/io/asciireader.py index f8232b1..dddf87d 100644 --- a/src/nmreval/io/asciireader.py +++ b/src/nmreval/io/asciireader.py @@ -15,7 +15,7 @@ NUMBERRE = re.compile(r'[0-9]\.*[0-9]*[Ee]*[+-]*[0-9]*') class AsciiReader: - delimiters = ['\t', ' ', ','] + # delimiters = ['\t', ' ', ','] def __init__(self, fname): self.fname = None @@ -49,7 +49,8 @@ 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.rstrip('\n\t\r, ') - line = re.split(r'[\s,;]', line) + line = re.sub(r'[\t ;,] *', ';', line) + line = line.split(';') try: comment_start = line.index('#') diff --git a/src/nmreval/io/dsc.py b/src/nmreval/io/dsc.py index 2be1fa4..b531bd4 100644 --- a/src/nmreval/io/dsc.py +++ b/src/nmreval/io/dsc.py @@ -292,7 +292,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[1], fill_value='extrapolate')(sample_data[2]) + empty_y = interp1d(empty_data[2]-empty_data[2, 0], empty_data[1], fill_value='extrapolate')(sample_data[2, 0]) sample_data[1] -= empty_y drift_value = sample_data.copy()[(2, 1), :] diff --git a/src/nmreval/math/interpol.py b/src/nmreval/math/interpol.py index 81a5b31..7d139cd 100644 --- a/src/nmreval/math/interpol.py +++ b/src/nmreval/math/interpol.py @@ -24,8 +24,8 @@ def interpolate(data, new_x, xlog=False, ylog=False, kind='cubic', extrapolate=T new_y = f(new_x) if ylog: - ret_val.set_data(x=new_x, y=10**new_y, y_err=None) + ret_val.set_data(x=new_x, y=10**new_y, y_err=0) else: - ret_val.set_data(x=new_x, y=new_y, y_err=None) + ret_val.set_data(x=new_x, y=new_y, y_err=0) return ret_val diff --git a/src/nmreval/models/bds.py b/src/nmreval/models/bds.py index 137c5ab..7e770e7 100644 --- a/src/nmreval/models/bds.py +++ b/src/nmreval/models/bds.py @@ -54,14 +54,35 @@ class HavriliakNegamiBDS(_AbstractBDS): name = 'Havriliak-Negami' equation = r'\Delta\epsilon / [1-(i\omega\tau)^{\gamma}]^{\alpha}' params = _AbstractBDS.params + [r'\alpha', r'\gamma'] - bounds = _AbstractBDS.bounds + [(0, 1), (0, 1)] + bounds = _AbstractBDS.bounds + [(0, 1), (0, None)] susceptibility = HavriliakNegami.susceptibility +class HavriliakNegamiAlphaGammaBDS: + type = 'Dielectric Spectroscopy' + name = 'Havriliak-Negami (ind. slopes)' + equation = r'\Delta\epsilon / [1-(i\omega\tau)^{\gamma}]^{\alpha}' + params = [r'\Delta\epsilon', r'\tau_{0}', r'\alpha', r'\alpha\gamma'] + bounds = [(0, None), (0, None), (0, 1), (0, 1)] + iscomplex = True + + @staticmethod + def func(x, deps, tau, alpha, alphagamma, complex_mode: int = 0, **kwargs): + chi = deps * HavriliakNegami.susceptibility(2*np.pi*x, tau, alpha, alphagamma/alpha, **kwargs) + if complex_mode == 0: + return chi + elif complex_mode == 1: + return chi.real + elif complex_mode == 2: + return chi.imag + else: + raise ValueError(f'{complex_mode!r} is not 0, 1, 2') + + class KWWBDS(_AbstractBDS): name = 'KWW' params = _AbstractBDS.params + [r'\beta'] - bounds = _AbstractBDS.bounds + [(0, 1)] + bounds = _AbstractBDS.bounds + [(0.1, 1)] susceptibility = KWW.susceptibility diff --git a/src/nmreval/models/diffusion.py b/src/nmreval/models/diffusion.py index 32dba92..f99e97a 100644 --- a/src/nmreval/models/diffusion.py +++ b/src/nmreval/models/diffusion.py @@ -149,7 +149,6 @@ class DiffusionGradients: else: tm = t2 tp = x - # T2 decay happens twice q2 = (g1**2 - g2**2) * (nucleus * tp)**2 t_eff = (2 * tp) / 3 + tm diff --git a/src/nmreval/models/fieldcycling.py b/src/nmreval/models/fieldcycling.py index 4d5221e..a1082d9 100644 --- a/src/nmreval/models/fieldcycling.py +++ b/src/nmreval/models/fieldcycling.py @@ -66,7 +66,7 @@ class HavriliakNegamiFC(_AbstractFC): class KWWFC(_AbstractFC): name = 'KWW' params = _AbstractFC.params + [r'\beta'] - bounds = _AbstractFC.bounds + [(0, 1)] + bounds = _AbstractFC.bounds + [(0.1, 1)] relax = Relaxation(distribution=KWW) diff --git a/src/nmreval/utils/text.py b/src/nmreval/utils/text.py index 57ff30a..7a8e8ba 100644 --- a/src/nmreval/utils/text.py +++ b/src/nmreval/utils/text.py @@ -80,6 +80,10 @@ def _replace_delims(text, src, dest): return text +def _replace_agr_controls(text: str): + return re.sub(r'\\[hvzfx]\{(\d*.?\d+.?)?\}', '', text) + + def convert(text: str, old: str = 'tex', new: str = 'html', brackets: bool = True): t = {'latex': 0, 'tex': 0, 'html': 1, 'agr': 2, 'plain': 3, 'str': 3} @@ -101,5 +105,8 @@ def convert(text: str, old: str = 'tex', new: str = 'html', brackets: bool = Tru if idx_out == 3 and not brackets: text = text.replace('{', '').replace('}', '') + if idx_in == 2: + text = _replace_agr_controls(text) + return text diff --git a/src/resources/_ui/apod_dialog.ui b/src/resources/_ui/apod_dialog.ui index 03c5c1b..5585d7b 100644 --- a/src/resources/_ui/apod_dialog.ui +++ b/src/resources/_ui/apod_dialog.ui @@ -36,7 +36,7 @@ 3 - + 0 @@ -46,7 +46,7 @@ - + @@ -98,9 +98,9 @@ - PlotWidget + NMRPlotWidget QGraphicsView -
pyqtgraph
+
..lib.graph_items
diff --git a/src/resources/_ui/baseline_dialog.ui b/src/resources/_ui/baseline_dialog.ui index f047d0f..6bfaec6 100644 --- a/src/resources/_ui/baseline_dialog.ui +++ b/src/resources/_ui/baseline_dialog.ui @@ -53,7 +53,7 @@
- + 0 @@ -66,9 +66,9 @@ - PlotWidget + NMRPlotWidget QGraphicsView -
pyqtgraph
+
..lib.graph_items
diff --git a/src/resources/_ui/dscfile_dialog.ui b/src/resources/_ui/dscfile_dialog.ui index c3ead1a..50af594 100644 --- a/src/resources/_ui/dscfile_dialog.ui +++ b/src/resources/_ui/dscfile_dialog.ui @@ -378,7 +378,7 @@ - + 0 @@ -394,7 +394,7 @@ - + 0 @@ -410,7 +410,7 @@ - + 0 @@ -426,7 +426,7 @@ - + 0 @@ -449,9 +449,9 @@ - PlotWidget + NMRPlotWidget QGraphicsView -
pyqtgraph
+
..lib.graph_items
diff --git a/src/resources/_ui/graph.ui b/src/resources/_ui/graph.ui index 02f573f..a433365 100644 --- a/src/resources/_ui/graph.ui +++ b/src/resources/_ui/graph.ui @@ -538,7 +538,7 @@
- + 0 @@ -552,16 +552,16 @@
- - PlotWidget - QGraphicsView -
pyqtgraph
-
QListWidgetSelect QListWidget
..lib.listwidget
+ + NMRPlotWidget + QGraphicsView +
..lib.graph_items
+
logx_button diff --git a/src/resources/_ui/phase_corr_dialog.ui b/src/resources/_ui/phase_corr_dialog.ui index caec699..227d063 100644 --- a/src/resources/_ui/phase_corr_dialog.ui +++ b/src/resources/_ui/phase_corr_dialog.ui @@ -36,7 +36,7 @@ 3 - + 0 @@ -160,9 +160,9 @@ - PlotWidget + NMRPlotWidget QGraphicsView -
pyqtgraph
+
..lib.graph_items
diff --git a/src/resources/_ui/shift_scale_dialog.ui b/src/resources/_ui/shift_scale_dialog.ui index 6f7939b..defeb9c 100644 --- a/src/resources/_ui/shift_scale_dialog.ui +++ b/src/resources/_ui/shift_scale_dialog.ui @@ -317,7 +317,7 @@ 3 - + 0 @@ -503,9 +503,9 @@ - PlotWidget + NMRPlotWidget QGraphicsView -
pyqtgraph
+
..lib.graph_items
SciSpinBox diff --git a/src/resources/_ui/t1dialog.ui b/src/resources/_ui/t1dialog.ui index e0438c1..b406232 100644 --- a/src/resources/_ui/t1dialog.ui +++ b/src/resources/_ui/t1dialog.ui @@ -451,6 +451,9 @@
+ + false + Use minimon interpolation diff --git a/src/resources/_ui/tnmh_dialog.ui b/src/resources/_ui/tnmh_dialog.ui index 7345efc..1961825 100644 --- a/src/resources/_ui/tnmh_dialog.ui +++ b/src/resources/_ui/tnmh_dialog.ui @@ -100,7 +100,7 @@ - + 0 @@ -145,10 +145,10 @@ - + - + 0 @@ -277,7 +277,7 @@ - + @@ -427,16 +427,16 @@ - - PlotWidget - QGraphicsView -
pyqtgraph
-
QListWidgetSelect QListWidget
..lib.listwidget
+ + NMRPlotWidget + QGraphicsView +
..lib.graph_items
+