from __future__ import annotations from pathlib import Path import numpy as np from pyqtgraph import PlotDataItem from nmreval.data import DSC from nmreval.io.dsc import Cyclohexane, DSCCalibrator, DSCSample from ..Qt import QtWidgets, QtCore from .._py.dscfile_dialog import Ui_Dialog class QDSCReader(QtWidgets.QDialog, Ui_Dialog): data_read = QtCore.pyqtSignal(list) file_ext = ['.txt', '.dsc'] def __init__(self, sample=None, empty=None, reference=None, parent=None): super().__init__(parent=parent) self.setupUi(self) self.calibrator = DSCCalibrator() self.current_run = (-1, 'h') self.sample_idx = None self.fname = None self.raw_graph.setLabel('bottom', text='T', units='K') self.raw_graph.setTitle('Raw data') self.raw_graph.addLegend() self.raw_sample = PlotDataItem(x=[], y=[], name='Sample') self.empty_sample = PlotDataItem(x=[], y=[], pen={'dash': (5, 5)}, name='Empty') self.raw_graph.addItem(self.raw_sample) self.raw_graph.addItem(self.empty_sample) self.end_graph.setTitle('End result') self.end_graph.setLabel('bottom', text='T', units='K') self.baseline_sample = PlotDataItem(x=[], y=[]) self.end_graph.addItem(self.baseline_sample) self.baseline_graph.setTitle('Time dependence') self.baseline_graph.setLabel('bottom', text='t', units='min') self.drift_sample = PlotDataItem(x=[], y=[]) self.slope_graph = PlotDataItem(x=[], y=[], pen={'color': 'r', 'dash': (5, 5)}) self.baseline_graph.addItem(self.drift_sample) self.baseline_graph.addItem(self.slope_graph) self.calib_graph.setTitle('Calibration') self.calib_graph.setLabel('bottom', text='T', units='K') self.ref_plotitems = [] self.sample = None if sample is not None: self.add_sample(sample) self.empty = None if empty is not None: self.add_empty(empty=empty) self.references = [] if reference is not None: if isinstance(reference, (str, Path, DSCSample)): reference = [reference] for r in reference: self.add_reference(ref=r) def __call__(self, fname: Path | str): self.clear_plots() self.add_sample(fname) return self def add_sample(self, fname: Path | str): self.sample = self.calibrator.set_measurement(fname, mode='sample') self.fname = self.sample.fname self.step_listWidget.clear() for opts in self.sample.steps: item = QtWidgets.QListWidgetItem() item.setFlags( QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsUserCheckable ) item.setCheckState(QtCore.Qt.CheckState.Unchecked) if opts[0] == 'i': item.setFlags(QtCore.Qt.ItemFlag.NoItemFlags) item.setText(f'{opts[1]:.2f} K for {opts[2] / 60:.0f} min') else: item.setFlags( QtCore.Qt.ItemFlag.ItemIsEnabled | QtCore.Qt.ItemFlag.ItemIsSelectable | QtCore.Qt.ItemFlag.ItemIsUserCheckable ) item.setText(f'{opts[2]:.2f} K to {opts[3]:.2f} K with {opts[1]} K/min') self.step_listWidget.addItem(item) @QtCore.pyqtSlot(name='on_loadempty_button_clicked') def add_empty(self, empty=None): if empty is None: empty, _ = QtWidgets.QFileDialog.getOpenFileName(directory=str(self.fname.parent)) if empty: self.empty = self.calibrator.set_measurement(empty, mode='empty') # avoid ValueError breaking data update if self.empty.fname.is_relative_to(Path.home()): self.empty_label.setText('~/' + str(self.empty.fname.relative_to(Path.home()))) else: self.empty_label.setText(str(self.empty.fname)) self.update_plots() @QtCore.pyqtSlot(name='on_delempty_button_clicked') def remove_empty(self): self.empty_label.setText('No empty measurement') self.calibrator.empty = None self.update_plots() @QtCore.pyqtSlot(name='on_ref_add_pushButton_clicked') def add_reference(self, ref: str | Path = None): if ref is None: ref, _ = QtWidgets.QFileDialog.getOpenFileName(directory=str(self.fname.parent)) if ref: ref = self.calibrator.set_measurement(ref, mode='reference') self.references.append(ref) item = QtWidgets.QTableWidgetItem(str(ref.fname.name)) item.setData(QtCore.Qt.ItemDataRole.UserRole, ref.fname) item.setFlags(QtCore.Qt.ItemFlag.ItemIsEnabled) rowcnt = self.reference_tableWidget.rowCount() self.reference_tableWidget.setRowCount(rowcnt+1) self.reference_tableWidget.setItem(rowcnt, 0, item) self.reference_tableWidget.setCellWidget(rowcnt, 1, ReferenceComboBox()) self.reference_tableWidget.resizeColumnsToContents() self.update_plots() @QtCore.pyqtSlot(name='on_ref_remove_pushButton_clicked') def remove_reference(self): idx = self.reference_tableWidget.currentRow() self.calibrator.remove_reference(self.reference_tableWidget.item(idx, 0).data(QtCore.Qt.ItemDataRole.UserRole)) self.reference_tableWidget.removeRow(idx) self.update_plots() @QtCore.pyqtSlot(QtWidgets.QListWidgetItem, name='on_step_listWidget_itemChanged') def select_run(self, item: QtWidgets.QListWidgetItem): idx = self.step_listWidget.indexFromItem(item).row() self.step_listWidget.blockSignals(True) for row in range(self.step_listWidget.count()): if idx == row: continue self.step_listWidget.item(row).setCheckState(QtCore.Qt.CheckState.Unchecked) self.step_listWidget.blockSignals(False) if item.checkState() == QtCore.Qt.CheckState.Checked: mode, rate, _, _ = self.sample.steps[idx] self.current_run = (rate, mode) self.sample_idx = idx self.update_plots() else: self.current_run = (-1, 'h') self.sample_idx = None self.clear_plots() def get_data(self): if self.sample_idx is None: return rate = self.current_run[0] slope_type = { self.none_radioButton: None, self.isotherm_radioButton: 'iso', self.slope_radioButton: 'curve', }[self.buttonGroup.checkedButton()] limit = None if slope_type == 'curve': try: limit = float(self.limit1_lineedit.text())*60, float(self.limit2_lineedit.text())*60 except ValueError: limit = None try: raw_sample, drift_value, sample_data, empty_data, slope = self.calibrator.get_data(self.sample_idx, slope=slope_type, limits=limit) except ValueError as e: _msg = QtWidgets.QMessageBox.warning(self, f'Data collection with error', e.args[0]) return self.calibrator.ref_list = [] for row in range(self.reference_tableWidget.rowCount()): self.calibrator.ref_list.append(self.reference_tableWidget.cellWidget(row, 1).get_reference()) calib_x, calib_y, regions = self.calibrator.get_calibration(rate) drift_value[0] /= 60. slope[0] /= 60. if calib_x is not None: sample_data[0] *= calib_x[0] sample_data[0] += calib_x[1] if self.cp_checkBox.isChecked(): sample_data[1] *= calib_y return sample_data, raw_sample, empty_data, drift_value, slope, calib_x, calib_y, regions @QtCore.pyqtSlot(QtWidgets.QAbstractButton, name='on_buttonGroup_buttonClicked') @QtCore.pyqtSlot(int, name='on_cp_checkBox_stateChanged') @QtCore.pyqtSlot(str, name='on_limit1_lineedit_textChanged') @QtCore.pyqtSlot(str, name='on_limit2_lineedit_textChanged') def update_plots(self, _=None): res = self.get_data() if res is None: return sample_data, raw_sample, empty_data, drift_value, slope, calib_x, calib_y, regions = res self.raw_sample.setData(x=raw_sample[0], y=raw_sample[1]) self.drift_sample.setData(x=drift_value[0], y=drift_value[1]) self.slope_graph.setData(x=slope[0], y=slope[1]) if empty_data is not None: self.empty_sample.setData(x=empty_data[0], y=empty_data[1]) else: self.empty_sample.setData(x=[], y=[]) self.calib_graph.clear() if calib_x is not None: for ref_zoom, onset, grad_points in regions: ref_plot = PlotDataItem(x=ref_zoom[0], y=ref_zoom[1]) self.calib_graph.addItem(ref_plot) self.calib_graph.addItem(PlotDataItem(x=grad_points[0], y=grad_points[1], symbol='x')) view_limits = -np.max(ref_zoom[1])/10, np.max(ref_zoom[1]) * 1.1 cut_idx, = np.where((onset < view_limits[1]) & (onset > view_limits[0])) self.calib_graph.addItem(PlotDataItem(x=ref_zoom[0, cut_idx], y=onset[cut_idx], pen={'color': 'r', 'dash': (2, 5)})) self.calib_graph.addItem(PlotDataItem(x=[ref_zoom[0, 0], ref_zoom[0, -1]], y=[0, 0], pen={'color': 'r', 'dash': (2, 5)})) self.baseline_sample.setData(x=sample_data[0], y=sample_data[1]) def clear_plots(self): for plot in [self.raw_sample, self.baseline_sample, self.empty_sample, self.drift_sample, self.slope_graph]: plot.setData(x=[], y=[]) self.calib_graph.clear() def export_data(self, filesave=False, close_after=True): try: sample_data = self.get_data()[0] except TypeError: return if self.cp_checkBox.isChecked() and self.references: y_label = 'cp' else: y_label = 'q' rate, mode = self.current_run new_val = DSC(sample_data[0], sample_data[1], value=rate, name=f'{self.fname.stem} {rate}K-min ({mode}, {y_label})') if filesave: new_val.savetxt(self.fname.with_name(f'{self.fname.stem}_{rate}K-min_{y_label}{mode}.dat'.replace(' ', '_'))) close_after = False else: self.data_read.emit([new_val]) if close_after: super().accept() @QtCore.pyqtSlot(QtWidgets.QAbstractButton, name='on_buttonBox_clicked') def button_clicked(self, bttn: QtWidgets.QAbstractButton): bttn_value = self.buttonBox.standardButton(bttn) if bttn_value in (self.buttonBox.Ok, self.buttonBox.Apply, self.buttonBox.Save): self.export_data(filesave=bttn_value == self.buttonBox.Save, close_after=bttn_value == self.buttonBox.Ok) else: super().close() class ReferenceComboBox(QtWidgets.QComboBox): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.references = [Cyclohexane] for ref in self.references: self.addItem(ref.name) def get_reference(self): return self.references[self.currentIndex()]