From 7fe564a61eb28ecb08c9c6a0682855d0cad084de Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 25 Oct 2023 20:04:49 +0200 Subject: [PATCH] more interactive edit --- src/gui_qt/_py/apod_dialog.py | 188 +++++++-- src/gui_qt/data/container.py | 38 ++ src/gui_qt/data/signaledit/__init__.py | 2 +- src/gui_qt/data/signaledit/phase_dialog.py | 444 ++++++++++++++------- src/gui_qt/lib/undos.py | 15 + src/gui_qt/main/mainwindow.py | 33 +- src/gui_qt/main/management.py | 7 + src/nmreval/data/nmr.py | 4 + src/nmreval/data/points.py | 27 +- src/resources/_ui/apod_dialog.ui | 404 +++++++++++++++++-- 10 files changed, 921 insertions(+), 241 deletions(-) diff --git a/src/gui_qt/_py/apod_dialog.py b/src/gui_qt/_py/apod_dialog.py index 6a40632..1f64573 100644 --- a/src/gui_qt/_py/apod_dialog.py +++ b/src/gui_qt/_py/apod_dialog.py @@ -14,54 +14,167 @@ from PyQt5 import QtCore, QtGui, QtWidgets class Ui_ApodEdit(object): def setupUi(self, ApodEdit): ApodEdit.setObjectName("ApodEdit") - ApodEdit.resize(784, 484) + ApodEdit.resize(1144, 655) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth(ApodEdit.sizePolicy().hasHeightForWidth()) ApodEdit.setSizePolicy(sizePolicy) self.gridLayout = QtWidgets.QGridLayout(ApodEdit) - self.gridLayout.setContentsMargins(3, 3, 3, 3) + self.gridLayout.setContentsMargins(9, 9, 9, 9) self.gridLayout.setSpacing(3) self.gridLayout.setObjectName("gridLayout") - self.graphicsView = NMRPlotWidget(ApodEdit) + self.time_graph = NMRPlotWidget(ApodEdit) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.graphicsView.sizePolicy().hasHeightForWidth()) - self.graphicsView.setSizePolicy(sizePolicy) - self.graphicsView.setObjectName("graphicsView") - self.gridLayout.addWidget(self.graphicsView, 2, 0, 1, 1) - 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) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Fixed) - sizePolicy.setHorizontalStretch(0) - sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.apodcombobox.sizePolicy().hasHeightForWidth()) - self.apodcombobox.setSizePolicy(sizePolicy) - self.apodcombobox.setObjectName("apodcombobox") - self.gridLayout.addWidget(self.apodcombobox, 0, 0, 1, 1) - self.widget_layout = QtWidgets.QHBoxLayout() - self.widget_layout.setContentsMargins(-1, 6, -1, -1) - self.widget_layout.setSpacing(20) - self.widget_layout.setObjectName("widget_layout") - self.gridLayout.addLayout(self.widget_layout, 1, 0, 1, 2) + sizePolicy.setHeightForWidth(self.time_graph.sizePolicy().hasHeightForWidth()) + self.time_graph.setSizePolicy(sizePolicy) + self.time_graph.setObjectName("time_graph") + self.gridLayout.addWidget(self.time_graph, 2, 1, 1, 1) + self.freq_graph = NMRPlotWidget(ApodEdit) + self.freq_graph.setObjectName("freq_graph") + self.gridLayout.addWidget(self.freq_graph, 2, 2, 1, 1) self.buttonBox = QtWidgets.QDialogButtonBox(ApodEdit) self.buttonBox.setOrientation(QtCore.Qt.Horizontal) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel|QtWidgets.QDialogButtonBox.Ok) self.buttonBox.setObjectName("buttonBox") - self.gridLayout.addWidget(self.buttonBox, 4, 0, 1, 2) - self.eqn_label = QtWidgets.QLabel(ApodEdit) - sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Preferred) + self.gridLayout.addWidget(self.buttonBox, 4, 1, 1, 2) + self.widget = QtWidgets.QWidget(ApodEdit) + sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) - sizePolicy.setHeightForWidth(self.eqn_label.sizePolicy().hasHeightForWidth()) - self.eqn_label.setSizePolicy(sizePolicy) + sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth()) + self.widget.setSizePolicy(sizePolicy) + self.widget.setObjectName("widget") + self.verticalLayout = QtWidgets.QVBoxLayout(self.widget) + self.verticalLayout.setObjectName("verticalLayout") + self.baseline_box = QtWidgets.QCheckBox(self.widget) + self.baseline_box.setObjectName("baseline_box") + self.verticalLayout.addWidget(self.baseline_box) + self.shift_box = QtWidgets.QGroupBox(self.widget) + self.shift_box.setFlat(True) + self.shift_box.setCheckable(True) + self.shift_box.setChecked(False) + self.shift_box.setObjectName("shift_box") + self.gridLayout_4 = QtWidgets.QGridLayout(self.shift_box) + self.gridLayout_4.setContentsMargins(3, 3, 3, 3) + self.gridLayout_4.setSpacing(3) + self.gridLayout_4.setObjectName("gridLayout_4") + self.ls_lineedit = QtWidgets.QLineEdit(self.shift_box) + self.ls_lineedit.setObjectName("ls_lineedit") + self.gridLayout_4.addWidget(self.ls_lineedit, 1, 1, 1, 1) + self.ls_spinbox = QtWidgets.QSpinBox(self.shift_box) + self.ls_spinbox.setMaximum(999999) + self.ls_spinbox.setObjectName("ls_spinbox") + self.gridLayout_4.addWidget(self.ls_spinbox, 0, 1, 1, 1) + self.ls_combobox = QtWidgets.QComboBox(self.shift_box) + self.ls_combobox.setObjectName("ls_combobox") + self.ls_combobox.addItem("") + self.ls_combobox.addItem("") + self.gridLayout_4.addWidget(self.ls_combobox, 0, 0, 2, 1) + self.verticalLayout.addWidget(self.shift_box) + self.apod_box = QtWidgets.QGroupBox(self.widget) + self.apod_box.setFlat(True) + self.apod_box.setCheckable(True) + self.apod_box.setChecked(False) + self.apod_box.setObjectName("apod_box") + self.verticalLayout_2 = QtWidgets.QVBoxLayout(self.apod_box) + self.verticalLayout_2.setContentsMargins(3, 3, 3, 3) + self.verticalLayout_2.setSpacing(3) + self.verticalLayout_2.setObjectName("verticalLayout_2") + self.apodcombobox = QtWidgets.QComboBox(self.apod_box) + self.apodcombobox.setObjectName("apodcombobox") + self.verticalLayout_2.addWidget(self.apodcombobox) + self.eqn_label = QtWidgets.QLabel(self.apod_box) self.eqn_label.setIndent(3) self.eqn_label.setObjectName("eqn_label") - self.gridLayout.addWidget(self.eqn_label, 0, 1, 1, 1) + self.verticalLayout_2.addWidget(self.eqn_label) + self.widget_layout = QtWidgets.QHBoxLayout() + self.widget_layout.setContentsMargins(-1, 6, -1, -1) + self.widget_layout.setSpacing(20) + self.widget_layout.setObjectName("widget_layout") + self.verticalLayout_2.addLayout(self.widget_layout) + self.verticalLayout.addWidget(self.apod_box) + self.zerofill_box = QtWidgets.QGroupBox(self.widget) + self.zerofill_box.setFlat(True) + self.zerofill_box.setCheckable(True) + self.zerofill_box.setChecked(False) + self.zerofill_box.setObjectName("zerofill_box") + self.horizontalLayout = QtWidgets.QHBoxLayout(self.zerofill_box) + self.horizontalLayout.setContentsMargins(3, 3, 3, 3) + self.horizontalLayout.setSpacing(3) + self.horizontalLayout.setObjectName("horizontalLayout") + self.label = QtWidgets.QLabel(self.zerofill_box) + self.label.setObjectName("label") + self.horizontalLayout.addWidget(self.label) + self.zf_spinbox = QtWidgets.QSpinBox(self.zerofill_box) + self.zf_spinbox.setMinimum(1) + self.zf_spinbox.setMaximum(3) + self.zf_spinbox.setObjectName("zf_spinbox") + self.horizontalLayout.addWidget(self.zf_spinbox) + self.verticalLayout.addWidget(self.zerofill_box) + self.phase_box = QtWidgets.QGroupBox(self.widget) + self.phase_box.setFlat(True) + self.phase_box.setCheckable(True) + self.phase_box.setChecked(False) + self.phase_box.setObjectName("phase_box") + self.gridLayout_2 = QtWidgets.QGridLayout(self.phase_box) + self.gridLayout_2.setContentsMargins(3, 3, 3, 3) + self.gridLayout_2.setSpacing(3) + self.gridLayout_2.setObjectName("gridLayout_2") + self.label_3 = QtWidgets.QLabel(self.phase_box) + self.label_3.setObjectName("label_3") + self.gridLayout_2.addWidget(self.label_3, 1, 0, 1, 1) + self.label_4 = QtWidgets.QLabel(self.phase_box) + self.label_4.setObjectName("label_4") + self.gridLayout_2.addWidget(self.label_4, 2, 0, 1, 1) + self.ph0_spinbox = QtWidgets.QDoubleSpinBox(self.phase_box) + self.ph0_spinbox.setWrapping(True) + self.ph0_spinbox.setDecimals(1) + self.ph0_spinbox.setMinimum(-180.0) + self.ph0_spinbox.setMaximum(180.0) + self.ph0_spinbox.setSingleStep(0.5) + self.ph0_spinbox.setObjectName("ph0_spinbox") + self.gridLayout_2.addWidget(self.ph0_spinbox, 0, 1, 1, 1) + self.pivot_lineedit = QtWidgets.QLineEdit(self.phase_box) + self.pivot_lineedit.setObjectName("pivot_lineedit") + self.gridLayout_2.addWidget(self.pivot_lineedit, 2, 1, 1, 1) + self.label_2 = QtWidgets.QLabel(self.phase_box) + self.label_2.setObjectName("label_2") + self.gridLayout_2.addWidget(self.label_2, 0, 0, 1, 1) + self.ph1_spinbox = QtWidgets.QDoubleSpinBox(self.phase_box) + self.ph1_spinbox.setWrapping(True) + self.ph1_spinbox.setDecimals(2) + self.ph1_spinbox.setMinimum(-720.0) + self.ph1_spinbox.setMaximum(720.0) + self.ph1_spinbox.setSingleStep(0.05) + self.ph1_spinbox.setObjectName("ph1_spinbox") + self.gridLayout_2.addWidget(self.ph1_spinbox, 1, 1, 1, 1) + self.verticalLayout.addWidget(self.phase_box) + self.ft_box = QtWidgets.QGroupBox(self.widget) + self.ft_box.setCheckable(True) + self.ft_box.setChecked(False) + self.ft_box.setObjectName("ft_box") + self.verticalLayout_3 = QtWidgets.QVBoxLayout(self.ft_box) + self.verticalLayout_3.setContentsMargins(3, 3, 3, 3) + self.verticalLayout_3.setSpacing(3) + self.verticalLayout_3.setObjectName("verticalLayout_3") + self.phase_before_button = QtWidgets.QRadioButton(self.ft_box) + self.phase_before_button.setChecked(True) + self.phase_before_button.setObjectName("phase_before_button") + self.buttonGroup = QtWidgets.QButtonGroup(ApodEdit) + self.buttonGroup.setObjectName("buttonGroup") + self.buttonGroup.addButton(self.phase_before_button) + self.verticalLayout_3.addWidget(self.phase_before_button) + self.phase_after_button = QtWidgets.QRadioButton(self.ft_box) + self.phase_after_button.setObjectName("phase_after_button") + self.buttonGroup.addButton(self.phase_after_button) + self.verticalLayout_3.addWidget(self.phase_after_button) + self.verticalLayout.addWidget(self.ft_box) + spacerItem = QtWidgets.QSpacerItem(20, 40, QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) + self.verticalLayout.addItem(spacerItem) + self.gridLayout.addWidget(self.widget, 2, 0, 1, 1) self.retranslateUi(ApodEdit) self.buttonBox.accepted.connect(ApodEdit.accept) # type: ignore @@ -71,5 +184,22 @@ class Ui_ApodEdit(object): def retranslateUi(self, ApodEdit): _translate = QtCore.QCoreApplication.translate ApodEdit.setWindowTitle(_translate("ApodEdit", "Apodization")) + self.baseline_box.setText(_translate("ApodEdit", "Baseline")) + self.shift_box.setTitle(_translate("ApodEdit", "Shift")) + self.ls_lineedit.setText(_translate("ApodEdit", "0")) + self.ls_combobox.setItemText(0, _translate("ApodEdit", "Points")) + self.ls_combobox.setItemText(1, _translate("ApodEdit", "Seconds")) + self.apod_box.setTitle(_translate("ApodEdit", "Apodization")) self.eqn_label.setText(_translate("ApodEdit", "TextLabel")) + self.zerofill_box.setTitle(_translate("ApodEdit", "Zero fill")) + self.label.setText(_translate("ApodEdit", "Double length")) + self.zf_spinbox.setSuffix(_translate("ApodEdit", "x")) + self.phase_box.setTitle(_translate("ApodEdit", "Phase correction")) + self.label_3.setText(_translate("ApodEdit", "Phase 1")) + self.label_4.setText(_translate("ApodEdit", "Pivot")) + self.pivot_lineedit.setText(_translate("ApodEdit", "0")) + self.label_2.setText(_translate("ApodEdit", "Phase 0")) + self.ft_box.setTitle(_translate("ApodEdit", "Fourier transform")) + self.phase_before_button.setText(_translate("ApodEdit", "before phase correction")) + self.phase_after_button.setText(_translate("ApodEdit", "after phase correction")) from ..lib.graph_items import NMRPlotWidget diff --git a/src/gui_qt/data/container.py b/src/gui_qt/data/container.py index 106eee3..ec78c6e 100644 --- a/src/gui_qt/data/container.py +++ b/src/gui_qt/data/container.py @@ -749,3 +749,41 @@ class SignalContainer(ExperimentContainer): self._update_actions() return self + + @plot_update + def edit_signal( + self: SignalContainer, + baseline: tuple[None | bool], + leftshift: tuple[None | float, str], + zerofill: tuple[None, int], + apod: tuple[None, list[float], type[object]], + phase: tuple[None | float, float, float], + fourier: tuple[None | bool] + ): + """ + Function for EditUndoCommand to call if a timesignal or spectra must be worked on. + This avoids to update the plot for every action we do and makes it slightly faster. + """ + + if baseline[0] is not None: + self._data.baseline() + + if leftshift[0] is not None: + self._data.shift(*leftshift) + + if zerofill[0] is not None: + self._data.zerofill(*zerofill) + + if apod[0] is not None: + self._data.apod(*apod) + + if fourier[0] is not None: + print(fourier) + if fourier[0]: + if phase[0] is not None: + self._data.manual_phase(*phase) + self.fourier() + else: + self.fourier() + if phase[0] is not None: + self._data.manual_phase(*phase) diff --git a/src/gui_qt/data/signaledit/__init__.py b/src/gui_qt/data/signaledit/__init__.py index b92851f..18efbfc 100644 --- a/src/gui_qt/data/signaledit/__init__.py +++ b/src/gui_qt/data/signaledit/__init__.py @@ -1,2 +1,2 @@ -from .phase_dialog import QApodDialog, QPhasedialog +from .phase_dialog import QPreviewDialog from .baseline_dialog import QBaselineDialog diff --git a/src/gui_qt/data/signaledit/phase_dialog.py b/src/gui_qt/data/signaledit/phase_dialog.py index 5962ed0..69a4d16 100644 --- a/src/gui_qt/data/signaledit/phase_dialog.py +++ b/src/gui_qt/data/signaledit/phase_dialog.py @@ -1,119 +1,43 @@ from __future__ import annotations -import numpy as np from pyqtgraph import mkPen -from numpy import inf, linspace -from numpy.fft import fft, fftfreq, fftshift +import numpy as np +from numpy import pi +from numpy.fft import fft, fftshift, fftfreq +from nmreval.data import FID, Spectrum from ...lib.pg_objects import PlotItem, LogInfiniteLine from nmreval.lib.importer import find_models from nmreval.math import apodization as apodization from nmreval.utils.text import convert -from ...Qt import QtCore, QtWidgets +from ...Qt import QtCore, QtWidgets, QtGui from ..._py.apod_dialog import Ui_ApodEdit -from ..._py.phase_corr_dialog import Ui_SignalEdit from ...lib.forms import FormWidget -class QPreviewDialogs(QtWidgets.QDialog): +class QPreviewDialog(QtWidgets.QDialog, Ui_ApodEdit): finished = QtCore.pyqtSignal(str, tuple) def __init__(self, parent=None): super().__init__(parent=parent) - self.data = [] - self.graphs = [] - - self.mode = '' - - def setRange(self, xlim: list, ylim: list, logmode: list[bool]): - self.graphicsView.getPlotItem().setLogMode(x=logmode[0], y=logmode[1]) - if logmode[0]: - xlim = [np.log10(x) for x in xlim] - if logmode[1]: - ylim = [np.log10(y) for y in ylim] - - self.graphicsView.setRange(xRange=xlim, yRange=ylim, padding=0, disableAutoRange=True) - - def add_data(self, x, y): - self.data.append((x, y)) - real_plt = PlotItem(x=x, y=y.real, pen=mkPen('b'), ) - imag_plt = PlotItem(x=x, y=y.imag, pen=mkPen('r')) - self.graphs.append((real_plt, imag_plt)) - self.graphicsView.addItem(real_plt) - self.graphicsView.addItem(imag_plt) - - def done(self, val): - self.cleanup() - super().done(val) - - def close(self): - self.cleanup() - super().close() - - def accept(self): - self.finished.emit(self.mode, self.get_value()) - super().accept() - - def get_value(self): - raise NotImplementedError - - def cleanup(self): - self.blockSignals(True) - - for line in self.graphs: - for g in line: - self.graphicsView.removeItem(g) - del g - - self.graphicsView.clear() - - self.data = [] - self.graphs = [] - - self.blockSignals(False) - - -class QPhasedialog(QPreviewDialogs, Ui_SignalEdit): - def __init__(self, parent=None): - super().__init__(parent=parent) self.setupUi(self) - self.mode = 'ph' + self.data = [] + self.graphs = [] + self._tmp_data_bl = [] + self._tmp_data_zf = [] + self._tmp_data_ls = [] + self._tmp_data_ap = [] + self._tmp_data_ph = [] self.pvt_line = LogInfiniteLine(pos=0, movable=True) - self.graphicsView.addItem(self.pvt_line) + self.freq_graph.addItem(self.pvt_line) self.pvt_line.sigPositionChanged.connect(self.move_line) - @QtCore.pyqtSlot(float, name='on_ph1slider_valueChanged') - @QtCore.pyqtSlot(float, name='on_ph0slider_valueChanged') - def _temp_phase(self, *args): - ph0, ph1, pvt = self.get_value() - self.pvt_line.setValue(pvt) + self.ls_lineedit.hide() - for i, (x, y) in enumerate(self.data): - phasecorr = np.exp(-1j * (ph0 + ph1*(x-pvt)/np.max(x))*np.pi/180.) - _y = y * phasecorr - - self.graphs[i][0].setData(x=x, y=_y.real) - self.graphs[i][1].setData(x=x, y=_y.imag) - - def get_value(self): - return float(self.ph0slider.text()), float(self.ph1slider.text()), float(self.pivot_lineedit.text()) - - def move_line(self, evt): - self.pivot_lineedit.setText(f'{evt.value():.5g}') - - -class QApodDialog(QPreviewDialogs, Ui_ApodEdit): - def __init__(self, parent=None): - super().__init__(parent=parent) - self.setupUi(self) - - self._limits = (-inf, inf), -inf - - self.apods = [] self.apods = find_models(apodization) self.apodcombobox.blockSignals(True) @@ -122,72 +46,246 @@ class QApodDialog(QPreviewDialogs, Ui_ApodEdit): self.apodcombobox.blockSignals(False) self.apod_graph = PlotItem(x=[], y=[]) - self.graphicsView.addItem(self.apod_graph) + self.time_graph.addItem(self.apod_graph) - self.mode = 'ap' + for g in [self.freq_graph, self.time_graph]: + pl = g.getPlotItem() + pl.hideButtons() + pl.setMenuEnabled(False) + + self._all_time = None + self._all_freq = None self.change_apodization(0) - def add_data(self, x, y): - real_plt = PlotItem(x=x, y=y.real, pen=mkPen('b')) - # imag_plt = (x=x, y=y.imag, pen=pg.mkPen('r')) - self.graphicsView.addItem(real_plt) - # self.graphicsView.addItem(imag_plt) + self.shift_box.clicked.connect(self._update_shift) + self.ls_spinbox.valueChanged.connect(self._update_shift) + self.ls_lineedit.setValidator(QtGui.QDoubleValidator()) + self.ls_lineedit.textChanged.connect(self._update_shift) + self.zerofill_box.clicked.connect(self._update_zf) + self.zf_spinbox.valueChanged.connect(self._update_zf) + + self.apod_box.clicked.connect(self._update_apod) + + self.phase_box.clicked.connect(self._update_phase) + self.ph0_spinbox.valueChanged.connect(self._update_phase) + self.ph1_spinbox.valueChanged.connect(self._update_phase) + self.pivot_lineedit.setValidator(QtGui.QDoubleValidator()) + self.pivot_lineedit.textChanged.connect(self._update_phase) + self.pivot_lineedit.textEdited.connect(lambda x: self.pvt_line.setValue(float(x))) + + def add_data(self: QPreviewDialog, data: FID | Spectrum) -> bool: + + if isinstance(data, FID): + if self._all_freq: + msg = QtWidgets.QMessageBox.warning(self, 'Mixed types', + 'Timesignals and spectra cannot be edited at the same time.') + return False + else: + self._all_time = True + self._all_freq = False + + elif isinstance(data, Spectrum): + if self._all_time: + msg = QtWidgets.QMessageBox.warning(self, 'Mixed types', + 'Timesignals and spectra cannot be edited at the same time.') + return False + else: + self._all_time = False + self._all_freq = True + + fid = data.copy() + spec = self._temp_fft(fid.x, fid.y) + + x_len = data.x.size + self.zf_spinbox.setMaximum(min(2**17//x_len, 3)) + + real_plt = PlotItem(x=fid.x, y=fid.y.real, pen=mkPen('b')) + imag_plt = PlotItem(x=fid.x, y=fid.y.imag, pen=mkPen('r')) + self.time_graph.addItem(imag_plt) + self.time_graph.addItem(real_plt) + + real_plt_fft = PlotItem(x=spec[0], y=spec[1].real, pen=mkPen('b')) + imag_plt_fft = PlotItem(x=spec[0], y=spec[1].imag, pen=mkPen('r')) + self.freq_graph.addItem(imag_plt_fft) + self.freq_graph.addItem(real_plt_fft) + + self.data.append(data) + for p in [self._tmp_data_bl, self._tmp_data_ls]: + p.append(data.y.copy()) + + for p in [self._tmp_data_zf, self._tmp_data_ap]: + p.append((data.x, data.y.copy())) + + self._tmp_data_ph.append((data.x, data.y, spec[0], spec[1])) + + self.graphs.append((real_plt, imag_plt, real_plt_fft, imag_plt_fft)) + + return True + + @QtCore.pyqtSlot(name='on_baseline_box_clicked') + def _update_bl(self): + if self.baseline_box.isChecked(): + for y in self._tmp_data_bl: + y -= y[int(-0.12*y.size):].mean() + else: + for i, d in enumerate(self.data): + self._tmp_data_bl[i] = d.y.copy() + + self._update_shift() + + def _update_shift(self): + if self.shift_box.isChecked(): + if self.ls_combobox.currentIndex() == 0: + num_points = self.ls_spinbox.value() + is_time = False + else: + num_points = float(self.ls_lineedit.text()) + is_time = True + + for i, y in enumerate(self._tmp_data_bl): + self._tmp_data_ls[i] = self._temp_leftshift(self.data[i].dx, y, num_points, is_time) + + else: + for i, y in enumerate(self._tmp_data_bl): + self._tmp_data_ls[i] = y + + self._update_zf() + + def _update_zf(self): + zf_padding = self.zf_spinbox.value() + + if self.zerofill_box.isChecked(): + for i, y in enumerate(self._tmp_data_ls): + self._tmp_data_zf[i] = self._temp_zerofill(self.data[i].x, y, zf_padding) + + else: + for i, y in enumerate(self._tmp_data_ls): + self._tmp_data_zf[i] = self.data[i].x, y + + self._update_apod() + + def _update_apod(self): + if self.apod_box.isChecked(): + model = self.apods[self.apodcombobox.currentIndex()] + p = self._get_parameter() + + x_limit = np.inf, -np.inf + y_limit = -np.inf + + for i, (x, y) in enumerate(self._tmp_data_zf): + self._tmp_data_ap[i] = x, y * model.apod(x, *p) + y_limit = max(y.real.max(), y_limit) + x_limit = min(x_limit[0], x.min()), max(x_limit[1], x.max()) + + _x_apod = np.linspace(*x_limit, num=150) + _y_apod = model.apod(_x_apod, *p) + self.apod_graph.setData(x=_x_apod, y=y_limit * _y_apod) + self.apod_graph.show() + + else: + for i, (x, y) in enumerate(self._tmp_data_zf): + self._tmp_data_ap[i] = x, y + + self.apod_graph.hide() + + self._update_phase() + + def _update_phase(self): + if self.phase_box.isChecked(): + pvt = float(self.pivot_lineedit.text()) + self.pvt_line.show() + ph0 = self.ph0_spinbox.value() + ph1 = self.ph1_spinbox.value() + + for i, (x, y) in enumerate(self._tmp_data_ap): + x_fft, y_fft = self._temp_fft(x, y) + + if ph0 != 0: + y = self._temp_phase(x, y, ph0, 0, 0) + y_fft = self._temp_phase(x, y_fft, ph0, ph1, pvt) + elif ph1 != 0: + y_fft = self._temp_phase(x, y_fft, ph0, ph1, pvt) + + self._tmp_data_ph[i] = x, y, x_fft, y_fft + + else: + self.pvt_line.hide() + for i, (x, y) in enumerate(self._tmp_data_ap): + self._tmp_data_ph[i] = x, y, *self._temp_fft(x, y) + + self._update_plots() + + def _update_plots(self): + for i, (x, y, xf, yf) in enumerate(self._tmp_data_ph): + self.graphs[i][0].setData(x=x, y=y.real) + self.graphs[i][1].setData(x=x, y=y.imag) + + self.graphs[i][2].setData(x=xf, y=yf.real) + self.graphs[i][3].setData(x=xf, y=yf.imag) + + @staticmethod + def _temp_phase(x: np.ndarray, y: np.ndarray, ph0: float, ph1: float, pvt: float) -> np.ndarray: + phase_correction = np.exp(-1j * (ph0 + ph1 * (x - pvt) / x.max()) * pi / 180.) + _y = y * phase_correction + + return _y + + @staticmethod + def _temp_zerofill(x: np.ndarray, y: np.ndarray, num_padding: int) -> tuple[np.ndarray, np.ndarray]: + length = x.size + factor = 2**num_padding + + _y = np.r_[y, np.zeros((factor-1) * length)] + + _temp_x = np.arange(1, (factor-1) * length+1) * (x[1]-x[0]) + np.max(x) + _x = np.r_[x, _temp_x] + + return _x, _y + + @staticmethod + def _temp_leftshift(dx: np.ndarray, y: np.ndarray, points: float | int, is_time: bool) -> np.ndarray: + if is_time: + points = int(points//dx) + _y = np.roll(y, -points) + _y[-points-1:] = 0 + + return _y + + @staticmethod + def _temp_fft(x: np.ndarray, y: np.ndarray) -> tuple[np.ndarray, np.ndarray]: y_fft = fftshift(fft(y)) x_fft = fftshift(fftfreq(len(x), d=x[1]-x[0])) - real_plt_fft = PlotItem(x=x_fft, y=y_fft.real, pen=mkPen('b')) - # imag_plt_fft = pg.PlotDataItem(x=x_fft, y=y_fft.imag, pen=pg.mkPen('b')) - self.graphicsView_2.addItem(real_plt_fft) - # self.graphicsView_2.addItem(imag_plt_fft) - self.graphs.append((real_plt, real_plt_fft)) - self.data.append((x, y, x_fft)) + return x_fft, y_fft - xlimits = (max(x.min(), self._limits[0][0]), min(x.max(), self._limits[0][1])) - ylimit = max(self._limits[1], y.real.max()) - self._limits = xlimits, ylimit + def move_line(self, evt): + self.pivot_lineedit.setText(f'{evt.value():.5g}') @QtCore.pyqtSlot(int, name='on_apodcombobox_currentIndexChanged') - def change_apodization(self, index): + def change_apodization(self, index: int) -> None: # delete old widgets self.eqn_label.setText(convert(self.apods[index].equation)) while self.widget_layout.count(): item = self.widget_layout.takeAt(0) + if isinstance(item, FormWidget): + item.disconnect() + try: item.widget().deleteLater() except AttributeError: pass # set up parameter widgets for new model - for k, v in enumerate(self.apods[index]().params): - widgt = FormWidget(name=v) - widgt.valueChanged.connect(self._temp_apod) - self.widget_layout.addWidget(widgt) + for k, v in enumerate(self.apods[index].params): + widget = FormWidget(name=v) + widget.value = 1 + widget.valueChanged.connect(self._update_apod) + self.widget_layout.addWidget(widget) self.widget_layout.addStretch() - self._temp_apod() - - def _temp_apod(self): - apodmodel = self.apods[self.apodcombobox.currentIndex()] - p = self._get_parameter() - - if self.data: - for i, (x, y, x_fft) in enumerate(self.data): - y2 = apodmodel.apod(x, *p) - _y = y2 * y - self.graphs[i][0].setData(x=x, y=_y.real) - # self.graphs[i][1].setData(y=_y.imag) - y_fft = fftshift(fft(_y)) - self.graphs[i][1].setData(x=x_fft, y=y_fft.real) - # self.graphs[i][3].setData(y=y_fft.imag) - - _x_apod = linspace(self._limits[0][0], self._limits[0][1]) - try: - _y_apod = apodmodel.apod(_x_apod, *p) - self.apod_graph.setData(x=_x_apod, y=self._limits[1]*_y_apod) - except IndexError: - pass + self._update_apod() def _get_parameter(self): p = [] @@ -201,8 +299,78 @@ class QApodDialog(QPreviewDialogs, Ui_ApodEdit): return p - def get_value(self): - apodmodel = self.apods[self.apodcombobox.currentIndex()] - p = self._get_parameter() + @QtCore.pyqtSlot(int, name='on_ls_combobox_currentIndexChanged') + def change_ls(self, idx: int) -> None: + self.ls_lineedit.setVisible(bool(idx)) + self.ls_spinbox.setVisible(not bool(idx)) - return p, apodmodel + @QtCore.pyqtSlot(bool, name='on_ft_checkbox_stateChanged') + def change_ft(self, state: bool): + self.ph1_spinbox.setEnabled(state) + self.pivot_lineedit.setEnabled(state) + + def cleanup(self): + self.blockSignals(True) + + for line in self.graphs: + for g in line: + self.time_graph.removeItem(g) + self.freq_graph.removeItem(g) + del g + + self.time_graph.clear() + self.freq_graph.clear() + + self._tmp_data_ap = [] + self._tmp_data_bl = [] + self._tmp_data_ls = [] + self._tmp_data_ph = [] + self._tmp_data_zf = [] + + self.data = [] + self.graphs = [] + + self.blockSignals(False) + + def get_value(self): + edits = [(None,), (None,), (None,), (None,), (None,), (None,)] + + if self.baseline_box.isChecked(): + edits[0] = (True,) + + if self.zerofill_box.isChecked(): + edits[2] = (self.zf_spinbox.value(),) + + if self.shift_box.isChecked(): + if self.ls_combobox.currentIndex() == 0: + edits[1] = (self.ls_spinbox.value(), 'pts') + else: + edits[1] = (float(self.ls_lineedit.text()), 'time') + + if self.apod_box.isChecked(): + edits[3] = (self._get_parameter(), self.apods[self.apodcombobox.currentIndex()]) + + if self.phase_box.isChecked(): + edits[4] = (self.ph0_spinbox.value(), self.ph1_spinbox.value(), float(self.pivot_lineedit.text())) + + if self.ft_box.isChecked(): + edits[5] = (self.phase_before_button.isChecked(),) + + return edits + + def exec(self): + self._prepare_ui() + return super().exec() + + def _prepare_ui(self): + """Stuff we have to do before showing the window but after all the data was added""" + + vb = self.freq_graph.getPlotItem().getViewBox() + vb.disableAutoRange(axis=vb.YAxis) + + 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) diff --git a/src/gui_qt/lib/undos.py b/src/gui_qt/lib/undos.py index 25f642a..683dedf 100644 --- a/src/gui_qt/lib/undos.py +++ b/src/gui_qt/lib/undos.py @@ -87,6 +87,21 @@ class ShiftCommand(QtWidgets.QUndoCommand): self.__data.apply('ls', self.__args) +class EditCommand(QtWidgets.QUndoCommand): + def __init__(self, data, *args): + super().__init__('Edit signal') + + self.__data = data + self.__arguments = args + self.__original = copy.deepcopy(self.__data.data) + + def undo(self): + self.__data.data = copy.deepcopy(self.__original) + + def redo(self): + self.__data.edit_signal(*self.__arguments) + + class NormCommand(QtWidgets.QUndoCommand): def __init__(self, data, mode): super().__init__('Normalize') diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index e20ae11..19af6cc 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -15,7 +15,7 @@ from nmreval.io.sessionwriter import NMRWriter from .management import UpperManagement from ..Qt import QtGui, QtPrintSupport from ..data.shift_graphs import QShift -from ..data.signaledit import QApodDialog, QBaselineDialog, QPhasedialog +from ..data.signaledit import QPreviewDialog, QBaselineDialog from ..dsc.glass_dialog import TgCalculator from ..fit.result import FitExtension, QFitResult from ..graphs.graphwindow import QGraphWindow @@ -164,7 +164,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.ac_group2.triggered.connect(self.change_fit_limits) self.t1action.triggered.connect(lambda: self._show_tab('t1_temp')) - self.action_edit.triggered.connect(lambda: self._show_tab('signal')) + self.action_edit.triggered.connect(self.do_preview) self.actionPick_position.triggered.connect(lambda: self._show_tab('pick')) self.actionIntegration.triggered.connect(lambda: self._show_tab('integrate')) self.action_FitWidget.triggered.connect(lambda: self._show_tab('fit')) @@ -214,7 +214,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.t1tauwidget.newData.connect(self.management.add_new_data) self.editsignalwidget.do_something.connect(self.management.apply) - self.editsignalwidget.preview_triggered.connect(self.do_preview) + # self.editsignalwidget.preview_triggered.connect(self.do_preview) self.action_sort_pts.triggered.connect(lambda: self.management.apply('sort', ())) self.action_calc_eps_derivative.triggered.connect(self.management.bds_deriv) @@ -784,29 +784,22 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): editor.finished.connect(self.management.apply) editor.exec() - @QtCore.pyqtSlot(str) - def do_preview(self, mode): + @QtCore.pyqtSlot() + def do_preview(self): + dialog = QPreviewDialog(self) - if mode == 'ap': - dialog = QApodDialog(parent=self) - elif mode == 'ph': - dialog = QPhasedialog(parent=self) - else: - raise ValueError('Unknown preview mode %s' % str(mode)) - - dialog.setRange(*self.current_graph_widget.ranges, self.current_graph_widget.log) + success = True for sid in self.current_graph_widget.active: data_mode = self.management[sid].mode - tobeadded = False - if (data_mode == 'fid') or (data_mode == 'spectrum' and mode == 'ph'): - tobeadded = True + if data_mode in ('fid', 'spectrum'): + success = dialog.add_data(self.management[sid].data) - if tobeadded: - dialog.add_data(*self.management.get_data(sid, xy_only=True)) + if not success: + break - if dialog.exec() == QtWidgets.QDialog.Accepted: - self.management.apply(mode, dialog.get_value()) + if success and dialog.exec() == QtWidgets.QDialog.Accepted: + self.management.edit_signals(dialog.get_value()) @QtCore.pyqtSlot(name='on_actionMove_between_plots_triggered') def move_sets_dialog(self): diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index 13e536e..07ee695 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -418,6 +418,13 @@ class UpperManagement(QtCore.QObject): self.undostack.push(single_undo) self.undostack.endMacro() + def edit_signals(self: UpperManagement, args: list[tuple]) -> None: + self.undostack.beginMacro('Edit signals') + for sid in self.graphs[self.current_graph]: + single_undo = EditCommand(self.data[sid], *args) + self.undostack.push(single_undo) + self.undostack.endMacro() + def cut(self): if self.current_graph: xlim, _ = self.graphs[self.current_graph].ranges diff --git a/src/nmreval/data/nmr.py b/src/nmreval/data/nmr.py index 8f5229a..cd5d339 100644 --- a/src/nmreval/data/nmr.py +++ b/src/nmreval/data/nmr.py @@ -62,6 +62,10 @@ class FID(Signal): return self + def manual_phase(self, ph0: float = 0., ph1: float = 0., pvt: float = 0): + """FID knows only how to phase correct in zeroth order""" + super().manual_phase(ph0=ph0) + def fourier(self) -> 'Spectrum': ft = np.fft.fftshift(np.fft.fft(self._y)) / self.dx freq = np.fft.fftshift(np.fft.fftfreq(self.length, self.dx)) diff --git a/src/nmreval/data/points.py b/src/nmreval/data/points.py index bf9a438..a9d8cba 100644 --- a/src/nmreval/data/points.py +++ b/src/nmreval/data/points.py @@ -319,20 +319,21 @@ class Points: if pts is None: pts = [] - for x in idx: - if isinstance(x, tuple): - x_idx = np.argmin(np.abs(self._x[self.mask] - (x[0]+x[1])/2)) - left_b = np.argmin(np.abs(self._x[self.mask] - x[0])) - right_b = np.argmin(np.abs(self._x[self.mask] - x[1])) - else: - x_idx = np.argmin(np.abs(self._x[self.mask]-x)) - left_b = int(max(0, x_idx - avg_range[0])) - right_b = int(min(len(self), x_idx + avg_range[1] + 1)) + if idx is not None: + for x in idx: + if isinstance(x, tuple): + x_idx = np.argmin(np.abs(self._x[self.mask] - (x[0]+x[1])/2)) + left_b = np.argmin(np.abs(self._x[self.mask] - x[0])) + right_b = np.argmin(np.abs(self._x[self.mask] - x[1])) + else: + x_idx = np.argmin(np.abs(self._x[self.mask]-x)) + left_b = int(max(0, x_idx - avg_range[0])) + right_b = int(min(len(self), x_idx + avg_range[1] + 1)) - if left_b < right_b: - pts.append([self._x[x_idx], *self._average(avg_mode, x_idx, left_b, right_b)]) - else: - pts.append([self._x[x_idx], self._y[x_idx], self._y_err[x_idx]]) + if left_b < right_b: + pts.append([self._x[x_idx], *self._average(avg_mode, x_idx, left_b, right_b)]) + else: + pts.append([self._x[x_idx], self._y[x_idx], self._y_err[x_idx]]) if special is not None: if special not in ['max', 'min', 'absmax', 'absmin']: diff --git a/src/resources/_ui/apod_dialog.ui b/src/resources/_ui/apod_dialog.ui index 5585d7b..e5ee569 100644 --- a/src/resources/_ui/apod_dialog.ui +++ b/src/resources/_ui/apod_dialog.ui @@ -6,8 +6,8 @@ 0 0 - 784 - 484 + 1144 + 655 @@ -21,22 +21,22 @@ - 3 + 9 - 3 + 9 - 3 + 9 - 3 + 9 3 - - + + 0 @@ -45,30 +45,10 @@ - - + + - - - - - 0 - 0 - - - - - - - - 20 - - - 6 - - - - + Qt::Horizontal @@ -78,20 +58,361 @@ - - + + - + 0 0 - - TextLabel - - - 3 - + + + + + Baseline + + + + + + + Shift + + + true + + + true + + + false + + + + 3 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + 0 + + + + + + + 999999 + + + + + + + + Points + + + + + Seconds + + + + + + + + + + + Apodization + + + true + + + true + + + false + + + + 3 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + + + + TextLabel + + + 3 + + + + + + + 20 + + + 6 + + + + + + + + + + Zero fill + + + true + + + true + + + false + + + + 3 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + Double length + + + + + + + x + + + 1 + + + 3 + + + + + + + + + + Phase correction + + + true + + + true + + + false + + + + 3 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + Phase 1 + + + + + + + Pivot + + + + + + + true + + + 1 + + + -180.000000000000000 + + + 180.000000000000000 + + + 0.500000000000000 + + + + + + + 0 + + + + + + + Phase 0 + + + + + + + true + + + 2 + + + -720.000000000000000 + + + 720.000000000000000 + + + 0.050000000000000 + + + + + + + + + + Fourier transform + + + true + + + false + + + + 3 + + + 3 + + + 3 + + + 3 + + + 3 + + + + + before phase correction + + + true + + + buttonGroup + + + + + + + after phase correction + + + buttonGroup + + + + + + + + + + Qt::Vertical + + + QSizePolicy::Expanding + + + + 20 + 40 + + + + + @@ -138,4 +459,7 @@ + + +