From 92a3933ed4655b7289c0ba32e579a1d71ac7be81 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Tue, 26 Dec 2023 16:05:19 +0000 Subject: [PATCH 1/2] bugfixes (#188) Bugfixes for several issues: closes #163, closes #151, closes #176, closes #138, closes #104 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/188 --- src/gui_qt/fit/fit_forms.py | 12 ++++++---- src/gui_qt/graphs/graphwindow.py | 34 ++++++++++++++++++++++++++ src/gui_qt/lib/mdiarea.py | 8 +++++++ src/gui_qt/main/mainwindow.py | 5 +++- src/nmreval/fit/minimizer.py | 41 +++++++++++++++++++++----------- src/nmreval/io/graceeditor.py | 16 ++++++++----- src/nmreval/utils/__init__.py | 1 - 7 files changed, 90 insertions(+), 27 deletions(-) diff --git a/src/gui_qt/fit/fit_forms.py b/src/gui_qt/fit/fit_forms.py index 6f2a424..624b13d 100644 --- a/src/gui_qt/fit/fit_forms.py +++ b/src/gui_qt/fit/fit_forms.py @@ -10,6 +10,10 @@ from ..lib.tables import TableWidget class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): + """ + Widget to show a global parameter + """ + value_requested = QtCore.pyqtSignal(object) value_changed = QtCore.pyqtSignal(str) state_changed = QtCore.pyqtSignal() @@ -73,11 +77,9 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): if bds is not None: self.set_bounds(*bds) - if fixed is not None: - self.fixed_check.setCheckState(QtCore.Qt.Unchecked if fixed else QtCore.Qt.Checked) + self.fixed_check.setCheckState(QtCore.Qt.CheckState.Unchecked if fixed else QtCore.Qt.CheckState.Checked) - if glob is not None: - self.global_checkbox.setCheckState(QtCore.Qt.Checked if glob else QtCore.Qt.Unchecked) + self.global_checkbox.setCheckState(QtCore.Qt.CheckState.Checked if glob else QtCore.Qt.CheckState.Unchecked) def get_parameter(self): try: @@ -126,7 +128,7 @@ class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter): is_text = True self.global_checkbox.setCheckState(False) - self.set_fixed(is_text) + self.set_fixed(is_text or self.fixed_check.isChecked()) class QSaveModelDialog(QtWidgets.QDialog, Ui_SaveDialog): diff --git a/src/gui_qt/graphs/graphwindow.py b/src/gui_qt/graphs/graphwindow.py index e38cb5f..6179aeb 100644 --- a/src/gui_qt/graphs/graphwindow.py +++ b/src/gui_qt/graphs/graphwindow.py @@ -27,6 +27,7 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): mouseDoubleClicked = QtCore.pyqtSignal() positionClicked = QtCore.pyqtSignal(tuple, bool) aboutToClose = QtCore.pyqtSignal(list) + newData = QtCore.pyqtSignal(list, str) counter = itertools.count() @@ -87,6 +88,9 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): self.bwbutton.toggled.connect(self.change_background) + self.setAcceptDrops(True) + self.graphic.installEventFilter(self) + def _init_gui(self): self.setWindowTitle('Graph ' + str(next(QGraphWindow.counter))) @@ -119,6 +123,34 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): for lineedit in [self.xmin_lineedit, self.xmax_lineedit, self.ymin_lineedit, self.ymax_lineedit]: lineedit.setValidator(QtGui.QDoubleValidator()) + def eventFilter(self, obj: QtCore.QObject, evt: QtCore.QEvent): + """ + Catch drag and Drop to prevent anything inside self.graphic to accept the events. + Without event filter, we cannot process it here and start file reading + """ + if evt.type() == QtCore.QEvent.Type.DragEnter: + evt.accept() + return True + + elif evt.type() == QtCore.QEvent.Type.Drop: + return self._handle_drop(evt) + + else: + return False + + def dropEvent(self, evt: QtGui.QDropEvent): + return self._handle_drop(evt) + + def _handle_drop(self, evt: QtGui.QDropEvent): + if evt.mimeData().hasUrls(): + files = [str(url.toLocalFile()) for url in evt.mimeData().urls()] + self.newData.emit(files, self.id) + + evt.accept() + return True + + return False + def __contains__(self, item: str): return item in self.sets @@ -304,6 +336,8 @@ class QGraphWindow(QtWidgets.QGraphicsView, Ui_GraphWindow): if item in self.graphic.items(): self.graphic.removeItem(item) + self.show_legend() + @QtCore.pyqtSlot(bool, name='on_imag_button_toggled') @QtCore.pyqtSlot(bool, name='on_real_button_toggled') def set_imag_visible(self, visible: bool): diff --git a/src/gui_qt/lib/mdiarea.py b/src/gui_qt/lib/mdiarea.py index 3dd6bbf..3338fdb 100644 --- a/src/gui_qt/lib/mdiarea.py +++ b/src/gui_qt/lib/mdiarea.py @@ -7,8 +7,11 @@ from ..graphs.graphwindow import QGraphWindow class MdiAreaTile(QtWidgets.QMdiArea): + newData = QtCore.pyqtSignal(list) + def __init__(self, parent=None): super().__init__(parent=parent) + self.setAcceptDrops(True) def tileSubWindowsVertically(self): window_list = self.subWindowList() @@ -47,3 +50,8 @@ class MdiAreaTile(QtWidgets.QMdiArea): if isinstance(wdgt, QGraphWindow) and wdgt.id == key: self.setActiveSubWindow(win) break + + def dropEvent(self, evt): + if evt.mimeData().hasUrls(): + files = [str(url.toLocalFile()) for url in evt.mimeData().urls()] + self.newData.emit(files) diff --git a/src/gui_qt/main/mainwindow.py b/src/gui_qt/main/mainwindow.py index 0ec24a3..87d6e2c 100644 --- a/src/gui_qt/main/mainwindow.py +++ b/src/gui_qt/main/mainwindow.py @@ -108,7 +108,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.fitlim_button.setIcon(get_icon('fit_region')) self.toolBar_fit.addWidget(self.fitlim_button) - self.area.dragEnterEvent = self.dragEnterEvent + # self.area.dragEnterEvent = self.dragEnterEvent while self.tabWidget.count() > 2: self.tabWidget.removeTab(self.tabWidget.count()-1) @@ -201,6 +201,8 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): self.management.unset_state.connect(lambda x: self.datawidget.uncheck_sets(x)) self.management.fitFinished.connect(self.show_fit_results) + self.area.newData.connect(lambda x: self.management.load_files(x)) + self.fit_dialog._management = self.management self.fit_dialog.preview_emit.connect(self.show_fit_preview) self.fit_dialog.fitStartSig.connect(self.start_fit) @@ -469,6 +471,7 @@ class NMRMainWindow(QtWidgets.QMainWindow, Ui_BaseWindow): w.mousePositionChanged.connect(self.mousemoved) w.aboutToClose.connect(self.management.delete_sets) w.positionClicked.connect(self.point_selected) + w.newData.connect(lambda x, y: self.management.load_files(x, new_plot=y)) w.show() graph_list = self.management.graphs.list() diff --git a/src/nmreval/fit/minimizer.py b/src/nmreval/fit/minimizer.py index e02042f..27e7492 100644 --- a/src/nmreval/fit/minimizer.py +++ b/src/nmreval/fit/minimizer.py @@ -234,27 +234,37 @@ class FitRoutine(object): # get variable parameter for fitter p0_k, lb_k, ub_k, var_pars_k = self._prep_data(data) - if mode == 'lsq': - self._least_squares_single(data, p0_k, lb_k, ub_k, var_pars_k) + if p0_k is None: + self.make_results(data, data.para_keys, var_pars_k, data.para_keys, (len(data.para_keys), len(data.para_keys)), + err=None, corr=None, partial_corr=None) + else: + if mode == 'lsq': + self._least_squares_single(data, p0_k, lb_k, ub_k, var_pars_k) - elif mode == 'nm': - self._nm_single(data, p0_k, lb_k, ub_k, var_pars_k) + elif mode == 'nm': + self._nm_single(data, p0_k, lb_k, ub_k, var_pars_k) - elif mode == 'odr': - # ODR takes no bounds - self._odr_single(data, p0_k, var_pars_k) + elif mode == 'odr': + # ODR takes no bounds + self._odr_single(data, p0_k, var_pars_k) else: data_pars, p0, lb, ub, var_pars = self._prep_global(data_groups, linked_parameter) - if mode == 'lsq': - self._least_squares_global(data_groups, p0, lb, ub, var_pars, data_pars) + if not p0: + for data_k, p_k in zip(data_groups, data_pars): + self.make_results(data_k, p_k, [], p_k, (len(p_k), len(p_k)), + err=None, corr=None, partial_corr=None) - elif mode == 'nm': - self._nm_global(data_groups, p0, lb, ub, var_pars, data_pars) + else: + if mode == 'lsq': + self._least_squares_global(data_groups, p0, lb, ub, var_pars, data_pars) - elif mode == 'odr': - self._odr_global(data_groups, p0, var_pars, data_pars) + elif mode == 'nm': + self._nm_global(data_groups, p0, lb, ub, var_pars, data_pars) + + elif mode == 'odr': + self._odr_global(data_groups, p0, var_pars, data_pars) self.unprep_run() @@ -291,7 +301,10 @@ class FitRoutine(object): vals.append([v_k.scaled_value, v_k.lb / v_k.scale, v_k.ub / v_k.scale]) var_pars.append(p_k) - pp, lb, ub = zip(*vals) + if vals: + pp, lb, ub = zip(*vals) + else: + pp = lb = ub = None return pp, lb, ub, var_pars diff --git a/src/nmreval/io/graceeditor.py b/src/nmreval/io/graceeditor.py index 47cbbac..cdde89c 100644 --- a/src/nmreval/io/graceeditor.py +++ b/src/nmreval/io/graceeditor.py @@ -115,6 +115,7 @@ class GraceEditor: return s def parse(self, filename: str | pathlib.Path): + self.clear() self.file = pathlib.Path(filename) # we start always with the header @@ -215,10 +216,9 @@ class GraceEditor: def _make_graph(self, line: str): m = self._RE_GRAPH_START.match(line) g_idx = int(m.group(1)) - if g_idx < len(self.graphs): + while g_idx < len(self.graphs): # this assumes that graphs are ordered in agr file even if missing, e.g. we read gß, g1, g3, ... - while g_idx != len(self.graphs): - self.graphs.append(GraceGraph(len(self.graphs))) + self.graphs.append(GraceGraph(len(self.graphs))) self.graphs.append(GraceGraph(g_idx)) @@ -354,8 +354,8 @@ class GraceHeader(list): class GraceProperties(list): _RE_ENTRY = re.compile(r'(?!.*(on|off)$)' # ignore lines that end with on or off r'@\s*(?P[gs]\d+)?\s*' # @ maybe followed by g0 or s12 - r'(?P[\w\s]*)\s+' # key: stops at last space unless comma-separated values - r'(?P(?:\s*[\d\w.+-]+\s*,)*\s*[\\{}\"\w.+\- ]+)', # one value, maybe more with commas + r'(?P[\w\s]*\w)*\s+' # key: stops at last space unless comma-separated values + r'(?P(?[gs]\d+)*\s*(?P[\w\s]*)\s+(on|off)') @@ -730,7 +730,11 @@ class GraceRegion(list): def _convert_to_value(parse_string): - tuples = parse_string.split(',') + if re.match(r'\".*\"', parse_string): + tuples = [parse_string] + else: + tuples = parse_string.split(',') + for i, v in enumerate(tuples): v = v.strip() diff --git a/src/nmreval/utils/__init__.py b/src/nmreval/utils/__init__.py index 64ec28c..8098c79 100755 --- a/src/nmreval/utils/__init__.py +++ b/src/nmreval/utils/__init__.py @@ -8,7 +8,6 @@ UNSIGNED_NUMBER_RE = re.compile(r'[-+]?\d*[+p.]?\d+([eE][-+]?\d+)?', re.MULTILIN def numbers_from_string(any_string: str) -> list[float]: - print(any_string) matches = [] for m in NUMBER_RE.finditer(any_string): matches.append(float(m.group().replace('p', '.'))) From 2cf94af2c4f357c5d784276de24b690844655f40 Mon Sep 17 00:00:00 2001 From: Dominik Demuth Date: Thu, 28 Dec 2023 10:24:34 +0000 Subject: [PATCH 2/2] fix point selection for values with all the same x (#190) bugfix for issue #189; closes #189 Co-authored-by: Dominik Demuth Reviewed-on: https://gitea.pkm.physik.tu-darmstadt.de/IPKM/nmreval/pulls/190 --- src/gui_qt/main/management.py | 6 ++++-- src/nmreval/data/points.py | 33 ++++++++++++++++++++------------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/gui_qt/main/management.py b/src/gui_qt/main/management.py index 69cb626..4b8b25c 100644 --- a/src/gui_qt/main/management.py +++ b/src/gui_qt/main/management.py @@ -742,8 +742,10 @@ class UpperManagement(QtCore.QObject): new_datasets[data_i.group] = [], [] new_x_axis, _temp = new_datasets[data_i.group] - new_x_axis.append(data_i.value) - _temp.append(data_i.points(params)) + pts = data_i.points(params) + if pts: + new_x_axis.append(data_i.value) + _temp.append(pts) key_list = [] for label, (new_x_axis, _temp) in new_datasets.items(): diff --git a/src/nmreval/data/points.py b/src/nmreval/data/points.py index a9d8cba..e4895f4 100644 --- a/src/nmreval/data/points.py +++ b/src/nmreval/data/points.py @@ -319,39 +319,46 @@ class Points: if pts is None: pts = [] + _tmp_x = self._x[self.mask] + _tmp_y = self._y[self.mask] 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])) + for idx_i in idx: + if isinstance(idx_i, tuple): + in_region = np.where((_tmp_x - idx_i[0] > 0) & (idx_i[1] - _tmp_x > 0))[0] + if len(in_region) > 0: + x_idx = in_region[in_region.size//2] + left_b = in_region[0] + right_b = in_region[-1] + 1 + else: + continue + else: - x_idx = np.argmin(np.abs(self._x[self.mask]-x)) + x_idx = np.argmin(np.abs(_tmp_x-idx_i)) 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)]) + pts.append([_tmp_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]]) + pts.append([_tmp_x[x_idx], _tmp_y[x_idx], self._y_err[x_idx]]) if special is not None: if special not in ['max', 'min', 'absmax', 'absmin']: raise ValueError('Parameter "special" must be "max", "min", "absmax", "absmin".') if special == 'max': - x_idx = np.argmax(self._y.real[self.mask]) + x_idx = np.argmax(_tmp_y.real) elif special == 'min': - x_idx = np.argmax(self._y.real[self.mask]) + x_idx = np.argmax(_tmp_y.real) elif special == 'absmax': - x_idx = np.argmax(np.abs(self._y[self.mask])) + x_idx = np.argmax(np.abs(_tmp_y)) else: - x_idx = np.argmin(np.abs(self._y[self.mask])) + x_idx = np.argmin(np.abs(_tmp_y)) left_b = int(max(0, x_idx - avg_range[0])) right_b = int(min(len(self), x_idx + avg_range[1] + 1)) - pts.append([self._x[self.mask][x_idx], *self._average(avg_mode, x_idx, left_b, right_b)]) + pts.append([_tmp_x[x_idx], *self._average(avg_mode, x_idx, left_b, right_b)]) return pts