From 5db6a9f2c50fddb397d868e6d4bb8c9c532238ca Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 19 Jul 2023 18:36:13 +0200 Subject: [PATCH 01/31] add HN model with independent slopes --- src/nmreval/models/bds.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/nmreval/models/bds.py b/src/nmreval/models/bds.py index 137c5ab..9356de9 100644 --- a/src/nmreval/models/bds.py +++ b/src/nmreval/models/bds.py @@ -54,10 +54,31 @@ 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'] From 609f13585512f008bd2c1c39ed06acbde2ae904c Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 19 Jul 2023 19:09:42 +0200 Subject: [PATCH 02/31] correcter preview for text files with mixed delimiters --- src/nmreval/io/asciireader.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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('#') From 0cf5d7eef98bce145cb3868d8ceab0f278665e40 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 20 Jul 2023 07:27:33 +0000 Subject: [PATCH 03/31] remove log(y) weight option from fit close #101 --- src/gui_qt/_py/fitdialog.py | 2 -- 1 file changed, 2 deletions(-) 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")) From 1ff462c4b1a3c4d1bff8c18d2a4bcb103b24f35c Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Fri, 21 Jul 2023 19:53:08 +0200 Subject: [PATCH 04/31] remove duplicate call of t1 calculate and t1 interpolation --- src/gui_qt/_py/t1dialog.py | 8 +++++--- src/gui_qt/main/mainwindow.py | 1 - src/gui_qt/nmr/t1widget.py | 2 ++ src/nmreval/io/dsc.py | 2 +- src/resources/_ui/t1dialog.ui | 3 +++ 5 files changed, 11 insertions(+), 5 deletions(-) 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/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 6973141..f5e9433 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) 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/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/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 From 13e6112c997cfea5d809b4c62bc41cf2f7feea8c Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 24 Jul 2023 11:08:42 +0000 Subject: [PATCH 05/31] overwrite error in interpolation; avoid mismatch in length part of #103 --- src/nmreval/math/interpol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From becc1a15a997fd99fa62de570032e95ba5194996 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 24 Jul 2023 17:38:55 +0200 Subject: [PATCH 06/31] move shift/scale to container; closes #102 --- src/gui_qt/data/container.py | 10 ++++++++++ src/gui_qt/main/management.py | 7 ++----- 2 files changed, 12 insertions(+), 5 deletions(-) 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/main/management.py b/src/gui_qt/main/management.py index 191fe7c..5e2966b 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -841,13 +841,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) From 402994e09b8bae2c79e9a23be27fee291634a9b4 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 25 Jul 2023 17:23:25 +0200 Subject: [PATCH 07/31] remove parameter limit; closes #106 --- src/gui_qt/fit/fit_forms.py | 3 +-- src/gui_qt/fit/fit_parameter.py | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index f0f63f7..0ef0591 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -20,10 +20,9 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): self.parametername.setText(label + ' ') validator = QtGui.QDoubleValidator() - validator.setDecimals(9) self.parameter_line.setValidator(validator) self.parameter_line.setText('1') - self.parameter_line.setMaximumWidth(60) + self.parameter_line.setMaximumWidth(240) self.lineEdit.setMaximumWidth(60) self.lineEdit_2.setMaximumWidth(60) diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index 682893e..95bf9db 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -298,7 +298,7 @@ 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) From 2b2c6e932da3a66892dec60b56783c2c7a7ee069 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 25 Jul 2023 17:31:42 +0200 Subject: [PATCH 08/31] set lower bound of FC and BDS KWW to 0.1 --- src/nmreval/models/bds.py | 2 +- src/nmreval/models/fieldcycling.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/nmreval/models/bds.py b/src/nmreval/models/bds.py index 9356de9..7e770e7 100644 --- a/src/nmreval/models/bds.py +++ b/src/nmreval/models/bds.py @@ -82,7 +82,7 @@ class HavriliakNegamiAlphaGammaBDS: 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/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) From 7762e299e4aa7f6fca8da28427df176865ea8193 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sat, 29 Jul 2023 17:06:05 +0200 Subject: [PATCH 09/31] manually setting graph limits when log-scaling to reduce memory usages; --- src/gui_qt/graphs/graphwindow.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/gui_qt/graphs/graphwindow.py b/src/gui_qt/graphs/graphwindow.py index 134a6cc..72ccdb6 100644 --- a/src/gui_qt/graphs/graphwindow.py +++ b/src/gui_qt/graphs/graphwindow.py @@ -439,6 +439,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() From b6136bc8ce402621ed8daaaff86958da16672cd4 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sat, 29 Jul 2023 18:47:15 +0200 Subject: [PATCH 10/31] subclass QTableWidget for change multiple checkstates --- src/gui_qt/fit/fit_forms.py | 3 ++- src/gui_qt/lib/tables.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index 0ef0591..ed215cf 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -6,6 +6,7 @@ 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): @@ -364,7 +365,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/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) + From 141e9f810afcbe4da615213d867005fad72110d1 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sat, 29 Jul 2023 18:47:57 +0200 Subject: [PATCH 11/31] convert removes agr controls --- src/gui_qt/io/gracereader.py | 4 +++- src/nmreval/utils/text.py | 7 +++++++ 2 files changed, 10 insertions(+), 1 deletion(-) 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/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 From dde7b7006d3864c636d1a2c31fecd4ae384d7e8f Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sat, 29 Jul 2023 21:25:22 +0200 Subject: [PATCH 12/31] overwrite logTickValues of AaxisItem to avoid to many minor ticks; maybe closes #110 --- src/gui_qt/graphs/graphwindow.py | 37 +++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/gui_qt/graphs/graphwindow.py b/src/gui_qt/graphs/graphwindow.py index 72ccdb6..7540b23 100644 --- a/src/gui_qt/graphs/graphwindow.py +++ b/src/gui_qt/graphs/graphwindow.py @@ -104,6 +104,10 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): self.plotItem.ctrl.logXCheck.blockSignals(True) self.plotItem.ctrl.logYCheck.blockSignals(True) + for orient in ['top', 'bottom', 'left', 'right']: + # BAD HACK!!! but seems to work, see function for explanation + self.plotItem.getAxis(orient).logTickValues = logTickValues + for lineedit in [self.xmin_lineedit, self.xmax_lineedit, self.ymin_lineedit, self.ymax_lineedit]: lineedit.setValidator(QtGui.QDoubleValidator()) @@ -144,7 +148,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) @@ -423,6 +427,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: @@ -766,3 +773,31 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): temp = self._fgcolor, self._bgcolor self.set_color(foreground=self._prev_colors[0], background=self._prev_colors[1]) self._prev_colors = temp + + +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(np.floor(minVal)) + v2 = int(np.ceil(maxVal)) + # major = list(range(v1+1, v2)) + minor = [] + + if v2 - v1 < 400: + for v in range(v1, v2): + minor.extend(v + np.log10(np.arange(1, 10))) + minor = [x for x in minor if x>minVal and x Date: Sun, 30 Jul 2023 16:18:18 +0200 Subject: [PATCH 13/31] list of active sets uses actual order instead order they were displayed; closes #111 --- src/gui_qt/graphs/graphwindow.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/src/gui_qt/graphs/graphwindow.py b/src/gui_qt/graphs/graphwindow.py index 7540b23..61f318c 100644 --- a/src/gui_qt/graphs/graphwindow.py +++ b/src/gui_qt/graphs/graphwindow.py @@ -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 = {} @@ -118,11 +118,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] @@ -157,6 +157,10 @@ 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] + def block(self, state: bool): self._block = state @@ -205,8 +209,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) @@ -250,8 +254,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), @@ -270,8 +274,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] @@ -293,7 +297,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) @@ -310,12 +314,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) @@ -677,7 +681,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 = [] From 63f4f822288cadc61d9d55fd5d86f78798993270 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sun, 30 Jul 2023 18:10:14 +0200 Subject: [PATCH 14/31] remove spurious print --- src/gui_qt/lib/pg_objects.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/gui_qt/lib/pg_objects.py b/src/gui_qt/lib/pg_objects.py index 9978031..6612848 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 From cf1565f7d38155fd6287330285ba988254cce5f0 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 31 Jul 2023 11:48:57 +0000 Subject: [PATCH 15/31] use symbol for length 1 data in shift/scale; fixes #112 --- src/gui_qt/data/shift_graphs.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) 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) From a5216b1eed044507459b1c469fdc97dc60a97f66 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 31 Jul 2023 20:17:31 +0200 Subject: [PATCH 16/31] constrain legend rect to be fully inside viewbox; simplest solution to fix #113 --- src/gui_qt/lib/pg_objects.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/gui_qt/lib/pg_objects.py b/src/gui_qt/lib/pg_objects.py index 6612848..fe706c9 100644 --- a/src/gui_qt/lib/pg_objects.py +++ b/src/gui_qt/lib/pg_objects.py @@ -5,7 +5,7 @@ from pyqtgraph import ( LinearRegionItem, mkBrush, mkColor, mkPen, PlotDataItem, - LegendItem, + LegendItem, ViewBox, ) from nmreval.lib.colors import BaseColor, Colors @@ -472,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) From d7eaff5f0be1338f458350766135135e4c27930c Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 1 Aug 2023 06:54:35 +0000 Subject: [PATCH 17/31] functioning apply button for tau calculation, part of #109 --- src/gui_qt/nmr/t1_from_tau.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/gui_qt/nmr/t1_from_tau.py b/src/gui_qt/nmr/t1_from_tau.py index 00e1f18..97b23a4 100644 --- a/src/gui_qt/nmr/t1_from_tau.py +++ b/src/gui_qt/nmr/t1_from_tau.py @@ -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() From 0eca306ebe4a248c8618288ca742639eb74b4f62 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 1 Aug 2023 18:00:23 +0200 Subject: [PATCH 18/31] assert that log scale is finite --- src/gui_qt/graphs/graphwindow.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/gui_qt/graphs/graphwindow.py b/src/gui_qt/graphs/graphwindow.py index 61f318c..7796b81 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 @@ -493,9 +493,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) From 347565089925dd3b5bdd16b109eda712f9b3b68a Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 1 Aug 2023 18:06:33 +0200 Subject: [PATCH 19/31] signal b/w change in graph was not connected --- src/gui_qt/graphs/graphwindow.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui_qt/graphs/graphwindow.py b/src/gui_qt/graphs/graphwindow.py index 7796b81..e3e19a8 100644 --- a/src/gui_qt/graphs/graphwindow.py +++ b/src/gui_qt/graphs/graphwindow.py @@ -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))) @@ -772,7 +774,6 @@ 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]) From b27d9b55ffc40d6c4d649f3809d2c2e708083ecc Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 1 Aug 2023 19:47:55 +0200 Subject: [PATCH 20/31] subclass every PlotWidget because logtickvalues --- src/gui_qt/_py/apod_dialog.py | 17 ++++---- src/gui_qt/_py/baseline_dialog.py | 15 +++---- src/gui_qt/_py/dscfile_dialog.py | 10 ++--- src/gui_qt/_py/graph.py | 4 +- src/gui_qt/_py/phase_corr_dialog.py | 12 +++--- src/gui_qt/_py/shift_scale_dialog.py | 15 +++---- src/gui_qt/_py/tnmh_dialog.py | 10 ++--- src/gui_qt/fit/result.py | 7 +++- src/gui_qt/graphs/graphwindow.py | 35 ++-------------- src/gui_qt/lib/graph_items.py | 53 +++++++++++++++++++++++++ src/gui_qt/lib/pg_objects.py | 2 +- src/resources/_ui/apod_dialog.ui | 8 ++-- src/resources/_ui/baseline_dialog.ui | 6 +-- src/resources/_ui/dscfile_dialog.ui | 12 +++--- src/resources/_ui/graph.ui | 12 +++--- src/resources/_ui/phase_corr_dialog.ui | 6 +-- src/resources/_ui/shift_scale_dialog.ui | 6 +-- src/resources/_ui/tnmh_dialog.ui | 18 ++++----- 18 files changed, 141 insertions(+), 107 deletions(-) create mode 100644 src/gui_qt/lib/graph_items.py 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/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/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/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 e3e19a8..ce40d71 100644 --- a/src/gui_qt/graphs/graphwindow.py +++ b/src/gui_qt/graphs/graphwindow.py @@ -106,10 +106,6 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): self.plotItem.ctrl.logXCheck.blockSignals(True) self.plotItem.ctrl.logYCheck.blockSignals(True) - for orient in ['top', 'bottom', 'left', 'right']: - # BAD HACK!!! but seems to work, see function for explanation - self.plotItem.getAxis(orient).logTickValues = logTickValues - for lineedit in [self.xmin_lineedit, self.xmax_lineedit, self.ymin_lineedit, self.ymax_lineedit]: lineedit.setValidator(QtGui.QDoubleValidator()) @@ -163,6 +159,10 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): 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 @@ -779,30 +779,3 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): self.set_color(foreground=self._prev_colors[0], background=self._prev_colors[1]) self._prev_colors = temp - -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(np.floor(minVal)) - v2 = int(np.ceil(maxVal)) - # major = list(range(v1+1, v2)) - minor = [] - - if v2 - v1 < 400: - for v in range(v1, v2): - minor.extend(v + np.log10(np.arange(1, 10))) - minor = [x for x in minor if x>minVal and x= 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 fe706c9..923ca88 100644 --- a/src/gui_qt/lib/pg_objects.py +++ b/src/gui_qt/lib/pg_objects.py @@ -5,7 +5,7 @@ from pyqtgraph import ( LinearRegionItem, mkBrush, mkColor, mkPen, PlotDataItem, - LegendItem, ViewBox, + LegendItem, ) from nmreval.lib.colors import BaseColor, Colors 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/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
+
From 258922772efd68ceef23ee7d4defa8e77109a50c Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 2 Aug 2023 19:58:02 +0200 Subject: [PATCH 21/31] change debian source --- AppImageBuilder.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 From ec8fafcc9c90445ef9a09a71249946ef6de9bb37 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sat, 5 Aug 2023 15:31:17 +0200 Subject: [PATCH 22/31] correct type for linenumbers in codeeditor --- src/gui_qt/lib/codeeditor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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() From 2fed4bb0bf3056cd71267a62dd1ad6dd0a40e4d9 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sat, 5 Aug 2023 17:53:24 +0200 Subject: [PATCH 23/31] fit creation dialog uses option name for kwargs --- src/gui_qt/fit/function_creation_dialog.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/gui_qt/fit/function_creation_dialog.py b/src/gui_qt/fit/function_creation_dialog.py index b63ee29..4c104bd 100644 --- a/src/gui_qt/fit/function_creation_dialog.py +++ b/src/gui_qt/fit/function_creation_dialog.py @@ -209,7 +209,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 +300,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}') From 783fe505ba68ab7016e8bace63076b6a1def29ef Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Sun, 6 Aug 2023 16:41:22 +0200 Subject: [PATCH 24/31] better check for valid class and argument names in fit function creation --- src/gui_qt/fit/function_creation_dialog.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui_qt/fit/function_creation_dialog.py b/src/gui_qt/fit/function_creation_dialog.py index 4c104bd..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() From 7febe5592933fedbcf3d194ffc7830e35e54ce62 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Mon, 7 Aug 2023 18:42:10 +0200 Subject: [PATCH 25/31] order of fits correspond order in graph, fit result window has correct order, see #109 --- src/gui_qt/main/management.py | 42 ++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index 5e2966b..b9c6f4a 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} @@ -431,8 +442,17 @@ class UpperManagement(QtCore.QObject): m_complex = model_p['complex'] - for set_id, set_params in model_p['parameter'].items(): + # 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] + data_i = self.data[set_id] + set_params = model_p['parameter'][set_id] + if we.lower() == 'deltay': we = data_i.y_err**2 @@ -635,7 +655,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] @@ -1004,7 +1024,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)) @@ -1015,7 +1035,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) @@ -1027,7 +1047,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)) @@ -1066,7 +1086,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) @@ -1093,7 +1113,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 From f0ee2073ad307177b187b4079dfc476d4e164e25 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 31 Aug 2023 19:34:13 +0200 Subject: [PATCH 26/31] fixed wrong order for nested fit functions; swapped exchange rates in Peschier model --- src/nmreval/fit/_meta.py | 11 +++++++++-- src/nmreval/models/diffusion.py | 3 +-- 2 files changed, 10 insertions(+), 4 deletions(-) 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/models/diffusion.py b/src/nmreval/models/diffusion.py index 4e03f92..f99e97a 100644 --- a/src/nmreval/models/diffusion.py +++ b/src/nmreval/models/diffusion.py @@ -125,7 +125,7 @@ class Peschier: q = nucleus*g*tp r1s, r1f = 1 / t1s, 1 / t1f - kf, ks = pf*k, (1-pf)*k + kf, ks = (1-pf)*k, pf*k a_plus = 0.5 * (d*q*q + kf + ks + r1f + r1s + np.sqrt((d*q*q + kf + r1f - ks - r1s)**2 + 4*kf*ks)) a_minu = 0.5 * (d*q*q + kf + ks + r1f + r1s - np.sqrt((d*q*q + kf + r1f - ks - r1s)**2 + 4*kf*ks)) @@ -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 From 4e865cd0c605198a4e9d89275fd56c905fa9ad36 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Wed, 6 Sep 2023 10:20:22 +0000 Subject: [PATCH 27/31] Update src/gui_qt/nmr/t1_from_tau.py Add Log-Gaussian to T1 calculation; closes #116 --- src/gui_qt/nmr/t1_from_tau.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/gui_qt/nmr/t1_from_tau.py b/src/gui_qt/nmr/t1_from_tau.py index 97b23a4..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 = [] From e2e52cebde3fe37a91e448f6206935e34b468bb9 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 7 Sep 2023 17:48:49 +0200 Subject: [PATCH 28/31] fixed overwrite of weight option for deltay, resulted in invalid weights; closes #117 --- src/gui_qt/main/management.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index b9c6f4a..86d1b41 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -434,7 +434,7 @@ 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']) @@ -453,8 +453,10 @@ class UpperManagement(QtCore.QObject): data_i = self.data[set_id] set_params = model_p['parameter'][set_id] - if we.lower() == 'deltay': + if we_option.lower() == 'deltay': we = data_i.y_err**2 + else: + we = we_option if m_complex is None or m_complex == 1: _y = data_i.y.real From a406908a698518cccd8a9761d755903cdd6ac4a0 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 7 Sep 2023 19:52:53 +0200 Subject: [PATCH 29/31] catch errors in fit preparation --- src/gui_qt/fit/fit_parameter.py | 3 +- src/gui_qt/main/mainwindow.py | 10 +-- src/gui_qt/main/management.py | 119 ++++++++++++++++++-------------- src/nmreval/fit/minimizer.py | 7 +- 4 files changed, 79 insertions(+), 60 deletions(-) diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index 95bf9db..3229f10 100644 --- a/src/gui_qt/fit/fit_parameter.py +++ b/src/gui_qt/fit/fit_parameter.py @@ -298,7 +298,8 @@ class ParameterSingleWidget(QtWidgets.QWidget): self._name = name self.label.setText(convert(name)) - self.label.setToolTip('If 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/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index f5e9433..6f1c47b 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -916,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 86d1b41..5470c44 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -424,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) @@ -436,67 +436,80 @@ class UpperManagement(QtCore.QObject): fit_mode = fit_options['fit_mode'] 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 - # 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] + m_complex = model_p['complex'] - data_i = self.data[set_id] - set_params = model_p['parameter'][set_id] + # 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] - if we_option.lower() == 'deltay': - we = data_i.y_err**2 - else: - we = we_option + data_i = self.data[set_id] + set_params = model_p['parameter'][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 + if we_option.lower() == 'deltay': + we = data_i.y_err**2 + else: + we = we_option - _x = data_i.x + 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 - 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])) + _x = data_i.x - 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 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])) - 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]) + 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) - self.fitter.add_data(d) + 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]) - model_globs = model_p['glob'] - if model_globs: - m.set_global_parameter(**model_p['glob']) + self.fitter.add_data(d) - for links_i in links: - self.fitter.set_link_parameter((models[links_i[0]], links_i[1]), - (models[links_i[2]], links_i[3])) + model_globs = model_p['glob'] + if model_globs: + 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) @@ -532,7 +545,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: """ @@ -1270,16 +1284,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/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index 57e1b56..d66db74 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -23,7 +23,7 @@ class FitAbortException(Exception): class FitRoutine(object): def __init__(self, mode='lsq'): - self._fitmethod = mode + self.fitmethod = mode self.data = [] self.fit_model = None self._no_own_model = [] @@ -169,10 +169,13 @@ class FitRoutine(object): logger.info('Fit aborted by user') self._abort = True - def run(self, mode='lsq'): + def run(self, mode: str=None): self._abort = False self.parameter = Parameters() + if mode is None: + mode = self.fitmethod + fit_groups, linked_parameter = self.prepare_links() for data_groups in fit_groups: From b8bab2af7b4cf0f4b07f5db1dbe866eb3e02f538 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Fri, 8 Sep 2023 17:54:27 +0200 Subject: [PATCH 30/31] fixed LG+CC spectral density calculation for omega=0; closes #118 --- src/nmreval/distributions/colecole.py | 3 +++ src/nmreval/distributions/loggaussian.py | 25 ++++++++++++------------ 2 files changed, 15 insertions(+), 13 deletions(-) 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): From dee1271fe188b0e44abe83bc95b9732610b3d1cc Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Fri, 8 Sep 2023 18:59:14 +0200 Subject: [PATCH 31/31] add action to context menu to replace single set fit parameter with general fit parameter --- src/gui_qt/fit/fit_forms.py | 2 ++ src/gui_qt/fit/fit_parameter.py | 8 ++++++++ src/gui_qt/lib/forms.py | 7 ++++++- 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index ed215cf..db7561d 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -13,6 +13,7 @@ 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) @@ -32,6 +33,7 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): self.checkBox.stateChanged.connect(self.enableBounds) self.global_checkbox.stateChanged.connect(lambda: self.state_changed.emit()) 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) diff --git a/src/gui_qt/fit/fit_parameter.py b/src/gui_qt/fit/fit_parameter.py index 3229f10..6c3ec60 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) @@ -148,6 +149,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() 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()