nmreval/src/gui_qt/io/dscreader.py
2024-02-06 18:21:51 +01:00

303 lines
11 KiB
Python

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()]