403 lines
17 KiB
Python
403 lines
17 KiB
Python
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
|
|
from .._py.fitresult import Ui_Dialog
|
|
from ..lib.pg_objects import PlotItem
|
|
|
|
|
|
class QFitResult(QtWidgets.QDialog, Ui_Dialog):
|
|
closed = QtCore.pyqtSignal(dict, list, str, bool, bool, list)
|
|
redoFit = QtCore.pyqtSignal(dict)
|
|
|
|
def __init__(self, results: list, management, parent=None):
|
|
super().__init__(parent=parent)
|
|
self.setupUi(self)
|
|
|
|
self._management = management
|
|
|
|
self.maxx_line.setValidator(QtGui.QDoubleValidator())
|
|
self.minx_line.setValidator(QtGui.QDoubleValidator())
|
|
self.numx_line.setValidator(QtGui.QIntValidator())
|
|
self.extrapolate_box.stateChanged.connect(lambda x: self.maxx_line.setEnabled(x))
|
|
self.extrapolate_box.stateChanged.connect(lambda x: self.minx_line.setEnabled(x))
|
|
self.extrapolate_box.stateChanged.connect(lambda x: self.numx_line.setEnabled(x))
|
|
|
|
self._previous_fits = {}
|
|
self._opts = []
|
|
self._results = {}
|
|
self.graph_opts = {}
|
|
self.last_idx = None
|
|
|
|
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)
|
|
|
|
self.resid_graph = PlotItem(x=[], y=[],
|
|
symbol='o', symbolPen=None, symbolBrush=mkBrush(color=(31, 119, 180)),
|
|
pen=None)
|
|
self.resid_graph_imag = PlotItem(x=[], y=[],
|
|
symbol='s', symbolPen=None, symbolBrush=mkBrush(color=(255, 127, 14)),
|
|
pen=None)
|
|
self.resid_plot.addItem(self.resid_graph)
|
|
self.resid_plot.addItem(self.resid_graph_imag)
|
|
|
|
self.data_graph = PlotItem(x=[], y=[],
|
|
symbol='o', symbolPen=None, symbolBrush=mkBrush(color=(31, 119, 180)),
|
|
pen=None)
|
|
self.data_graph_imag = PlotItem(x=[], y=[],
|
|
symbol='s', symbolPen=None, symbolBrush=mkBrush(color=(255, 127, 14)),
|
|
pen=None)
|
|
self.fit_plot.addItem(self.data_graph)
|
|
self.fit_plot.addItem(self.data_graph_imag)
|
|
|
|
self.fit_graph = PlotItem(x=[], y=[])
|
|
self.fit_graph_imag = PlotItem(x=[], y=[])
|
|
self.fit_plot.addItem(self.fit_graph)
|
|
self.fit_plot.addItem(self.fit_graph_imag)
|
|
|
|
self.cmap = RdBuCMap(vmin=-1, vmax=1)
|
|
|
|
self.graph_checkBox.stateChanged.connect(lambda x: self.graph_comboBox.setEnabled(x == QtCore.Qt.Unchecked))
|
|
|
|
self.logy_box.stateChanged.connect(lambda x: self.fit_plot.setLogMode(y=bool(x)))
|
|
self.logx_box.stateChanged.connect(lambda x: self.fit_plot.setLogMode(x=bool(x)))
|
|
self.resid_plot.setXLink(self.fit_plot)
|
|
|
|
self.set_results(results)
|
|
|
|
def __call__(self, results: list):
|
|
self._previous_fits = {}
|
|
self.sets_comboBox.blockSignals(True)
|
|
self.sets_comboBox.clear()
|
|
self.sets_comboBox.blockSignals(False)
|
|
self._results = {}
|
|
self._opts = {}
|
|
|
|
self.set_results(results)
|
|
|
|
def set_results(self, results: list):
|
|
self.sets_comboBox.blockSignals(True)
|
|
for res in results:
|
|
idx = res.idx
|
|
data_k = self._management.data[idx]
|
|
|
|
self._previous_fits[idx] = []
|
|
for fit in data_k.get_fits():
|
|
self._previous_fits[idx].append((fit.name, fit.statistics, fit.nobs - fit.nvar))
|
|
|
|
self.sets_comboBox.addItem(data_k.name, userData=idx)
|
|
self.sets_comboBox.blockSignals(False)
|
|
|
|
self._results = {res.idx: res for res in results}
|
|
self._opts = [(False, False) for _ in range(len(self._results))]
|
|
|
|
self.set_parameter(0)
|
|
|
|
def add_graphs(self, graphs: list):
|
|
self.graph_comboBox.clear()
|
|
for (graph_id, graph_name) in graphs:
|
|
self.graph_comboBox.addItem(graph_name, userData=graph_id)
|
|
|
|
@QtCore.pyqtSlot(int, name='on_sets_comboBox_currentIndexChanged')
|
|
def set_parameter(self, idx: int):
|
|
set_id = self.sets_comboBox.itemData(idx, QtCore.Qt.UserRole)
|
|
|
|
res = self._results[set_id]
|
|
self.param_tableWidget.setRowCount(len(res.parameter))
|
|
for j, (pkey, pvalue) in enumerate(res.parameter.items()):
|
|
name = pkey
|
|
p_header = QtWidgets.QTableWidgetItem(convert(name, 'tex', 'str', brackets=True))
|
|
self.param_tableWidget.setVerticalHeaderItem(j, p_header)
|
|
|
|
item_text = f'{pvalue.value:.4g}'
|
|
if pvalue.error is not None:
|
|
item_text += f' \u00b1 {pvalue.error:.4g}'
|
|
self.param_tableWidget.setItem(2*j+1, 0, QtWidgets.QTableWidgetItem('-'))
|
|
else:
|
|
self.param_tableWidget.setItem(2*j+1, 0, QtWidgets.QTableWidgetItem())
|
|
item = QtWidgets.QTableWidgetItem(item_text)
|
|
self.param_tableWidget.setItem(j, 0, item)
|
|
|
|
self.param_tableWidget.resizeColumnToContents(0)
|
|
self.show_results(idx)
|
|
|
|
@QtCore.pyqtSlot(int, name='on_reject_fit_checkBox_stateChanged')
|
|
@QtCore.pyqtSlot(int, name='on_del_prev_checkBox_stateChanged')
|
|
def change_opts(self, _):
|
|
idx = self.sets_comboBox.currentIndex()
|
|
|
|
self._opts[idx] = (self.reject_fit_checkBox.checkState() == QtCore.Qt.Checked,
|
|
self.del_prev_checkBox.checkState() == QtCore.Qt.Checked)
|
|
|
|
def show_results(self, idx):
|
|
set_id = self.sets_comboBox.itemData(idx, QtCore.Qt.UserRole)
|
|
self.set_plot(set_id)
|
|
self.set_correlation(set_id)
|
|
self.set_statistics(set_id)
|
|
self.reject_fit_checkBox.blockSignals(True)
|
|
self.reject_fit_checkBox.setChecked(self._opts[idx][0])
|
|
self.reject_fit_checkBox.blockSignals(False)
|
|
self.del_prev_checkBox.blockSignals(True)
|
|
self.del_prev_checkBox.setChecked(self._opts[idx][1])
|
|
self.del_prev_checkBox.blockSignals(False)
|
|
|
|
@QtCore.pyqtSlot(name='on_autoscale_box_clicked')
|
|
def reset_fit_ranges(self):
|
|
for i in range(self.sets_comboBox.count()):
|
|
graph_id = self.sets_comboBox.itemData(i)
|
|
self.graph_opts.pop(graph_id)
|
|
|
|
self.fit_plot.enableAutoRange()
|
|
|
|
def set_plot(self, idx: str):
|
|
if self.last_idx is not None:
|
|
self.graph_opts[self.last_idx] = (
|
|
self.fit_plot.viewRange(),
|
|
self.logx_box.isChecked(),
|
|
self.logy_box.isChecked(),
|
|
)
|
|
|
|
self.last_idx = idx
|
|
res = self._results[idx]
|
|
iscomplex = res.iscomplex
|
|
|
|
sub_funcs = res.sub(res.x)
|
|
for item in self.fit_plot.items[::-1]:
|
|
if item not in [self.data_graph, self.data_graph_imag, self.fit_graph, self.fit_graph_imag]:
|
|
self.fit_plot.removeItem(item)
|
|
|
|
if iscomplex:
|
|
self.data_graph.setData(x=res.x_data, y=res.y_data.real)
|
|
self.data_graph_imag.setData(x=res.x_data, y=res.y_data.imag)
|
|
self.fit_graph.setData(x=res.x, y=res.y.real)
|
|
self.fit_graph_imag.setData(x=res.x, y=res.y.imag)
|
|
self.resid_graph.setData(x=res.x_data, y=res.residual.real)
|
|
self.resid_graph_imag.setData(x=res.x_data, y=res.residual.imag)
|
|
|
|
for i, f in enumerate(sub_funcs):
|
|
item = PlotItem(x=f.x, y=f.y.real, pen=mkPen({'color': i, 'style': 2}))
|
|
self.fit_plot.addItem(item)
|
|
item = PlotItem(x=f.x, y=f.y.imag, pen=mkPen({'color': i, 'style': 2}))
|
|
self.fit_plot.addItem(item)
|
|
|
|
else:
|
|
self.resid_graph.setData(x=res.x_data, y=res.residual)
|
|
self.resid_graph_imag.setData(x=[], y=[])
|
|
self.data_graph.setData(x=res.x_data, y=res.y_data)
|
|
self.data_graph_imag.setData(x=[], y=[])
|
|
self.fit_graph.setData(x=res.x, y=res.y)
|
|
self.fit_graph_imag.setData(x=[], y=[])
|
|
|
|
for i, f in enumerate(sub_funcs):
|
|
item = PlotItem(x=f.x, y=f.y, pen=mkPen({'color': i, 'style': 2}))
|
|
self.fit_plot.addItem(item)
|
|
|
|
self.logx_box.blockSignals(True)
|
|
self.logx_box.setChecked(res.islog)
|
|
self.logx_box.blockSignals(False)
|
|
|
|
self.fit_plot.setLogMode(x=res.islog)
|
|
self.resid_plot.setLogMode(x=res.islog)
|
|
|
|
if idx in self.graph_opts:
|
|
view_range, logx, logy = self.graph_opts[idx]
|
|
self.fit_plot.setLogMode(x=logx, y=logy)
|
|
self.fit_plot.setRange(xRange=view_range[0], yRange=view_range[1], padding=0)
|
|
self.logx_box.blockSignals(True)
|
|
self.logx_box.setChecked(logx)
|
|
self.logx_box.blockSignals(False)
|
|
self.logy_box.blockSignals(True)
|
|
self.logy_box.setChecked(logy)
|
|
self.logy_box.blockSignals(False)
|
|
else:
|
|
self.fit_plot.enableAutoRange()
|
|
|
|
def set_correlation(self, idx: str):
|
|
while self.corr_tableWidget.rowCount():
|
|
self.corr_tableWidget.removeRow(0)
|
|
|
|
res = self._results[idx]
|
|
c = res.correlation_list()
|
|
for pi, pj, corr, pcorr in c:
|
|
cnt = self.corr_tableWidget.rowCount()
|
|
self.corr_tableWidget.insertRow(cnt)
|
|
self.corr_tableWidget.setItem(cnt, 0, QtWidgets.QTableWidgetItem(convert(pi, old='tex', new='str')))
|
|
self.corr_tableWidget.setItem(cnt, 1, QtWidgets.QTableWidgetItem(convert(pj, old='tex', new='str')))
|
|
|
|
for i, val in enumerate([corr, pcorr]):
|
|
if isnan(val):
|
|
val = 1000.
|
|
val_item = QtWidgets.QTableWidgetItem(f'{val:.4g}')
|
|
val_item.setBackground(self.cmap.color(val))
|
|
if abs(val) > 0.75:
|
|
val_item.setForeground(QtGui.QColor('white'))
|
|
self.corr_tableWidget.setItem(cnt, i+2, val_item)
|
|
|
|
self.corr_tableWidget.resizeColumnsToContents()
|
|
|
|
def set_statistics(self, idx: str):
|
|
while self.stats_tableWidget.rowCount():
|
|
self.stats_tableWidget.removeRow(0)
|
|
|
|
res = self._results[idx]
|
|
|
|
self.stats_tableWidget.setColumnCount(1 + len(self._previous_fits[idx]))
|
|
self.stats_tableWidget.setRowCount(len(res.statistics)+3)
|
|
|
|
it = QtWidgets.QTableWidgetItem(f'{res.dof}')
|
|
it.setFlags(it.flags() ^ QtCore.Qt.ItemIsEditable)
|
|
self.stats_tableWidget.setVerticalHeaderItem(0, QtWidgets.QTableWidgetItem('DoF'))
|
|
self.stats_tableWidget.setItem(0, 0, it)
|
|
|
|
for col, (name, _, dof) in enumerate(self._previous_fits[idx], start=1):
|
|
self.stats_tableWidget.setHorizontalHeaderItem(0, QtWidgets.QTableWidgetItem(name))
|
|
it = QtWidgets.QTableWidgetItem(f'{dof}')
|
|
it.setFlags(it.flags() ^ QtCore.Qt.ItemIsEditable)
|
|
self.stats_tableWidget.setItem(0, col, it)
|
|
|
|
for row, (k, v) in enumerate(res.statistics.items(), start=1):
|
|
self.stats_tableWidget.setVerticalHeaderItem(row, QtWidgets.QTableWidgetItem(k))
|
|
it = QtWidgets.QTableWidgetItem(f'{v:.4f}')
|
|
it.setFlags(it.flags() ^ QtCore.Qt.ItemIsEditable)
|
|
self.stats_tableWidget.setItem(row, 0, it)
|
|
|
|
best_idx = -1
|
|
best_val = v
|
|
for col, (_, stats, _) in enumerate(self._previous_fits[idx], start=1):
|
|
if k in ['adj. R^2', 'R^2']:
|
|
best_idx = col if best_val < stats[k] else max(0, best_idx)
|
|
else:
|
|
best_idx = col if best_val > stats[k] else max(0, best_idx)
|
|
it = QtWidgets.QTableWidgetItem(f'{stats[k]:.4f}')
|
|
it.setFlags(it.flags() ^ QtCore.Qt.ItemIsEditable)
|
|
self.stats_tableWidget.setItem(row, col, it)
|
|
|
|
if best_idx > -1:
|
|
self.stats_tableWidget.item(row, best_idx).setBackground(QtGui.QColor('green'))
|
|
self.stats_tableWidget.item(row, best_idx).setForeground(QtGui.QColor('white'))
|
|
|
|
row = self.stats_tableWidget.rowCount() - 2
|
|
self.stats_tableWidget.setVerticalHeaderItem(row, QtWidgets.QTableWidgetItem('F'))
|
|
self.stats_tableWidget.setItem(row, 0, QtWidgets.QTableWidgetItem('-'))
|
|
|
|
self.stats_tableWidget.setVerticalHeaderItem(row+1, QtWidgets.QTableWidgetItem('Pr(>F)'))
|
|
self.stats_tableWidget.setItem(row+1, 0, QtWidgets.QTableWidgetItem('-'))
|
|
|
|
for col, (_, stats, dof) in enumerate(self._previous_fits[idx], start=1):
|
|
f_value, prob_f = res.f_test(stats['chi^2'], dof)
|
|
it = QtWidgets.QTableWidgetItem(f'{f_value:.4g}')
|
|
it.setFlags(it.flags() ^ QtCore.Qt.ItemIsEditable)
|
|
self.corr_tableWidget.setItem(row, col, it)
|
|
|
|
it = QtWidgets.QTableWidgetItem(f'{prob_f:.4g}')
|
|
it.setFlags(it.flags() ^ QtCore.Qt.ItemIsEditable)
|
|
if prob_f < 0.05:
|
|
it.setBackground(QtGui.QColor('green'))
|
|
it.setForeground(QtGui.QColor('white'))
|
|
self.stats_tableWidget.setItem(row+1, col, it)
|
|
|
|
@QtCore.pyqtSlot(QtWidgets.QAbstractButton)
|
|
def on_buttonBox_clicked(self, button: QtWidgets.QAbstractButton):
|
|
button_type = self.buttonBox.standardButton(button)
|
|
|
|
if button_type == self.buttonBox.Retry:
|
|
self.redoFit.emit(self._results)
|
|
|
|
elif button_type == self.buttonBox.Ok:
|
|
graph = '-1'
|
|
if self.parameter_checkbox.isChecked():
|
|
if self.graph_checkBox.checkState() == QtCore.Qt.Checked:
|
|
graph = ''
|
|
else:
|
|
graph = self.graph_comboBox.currentData()
|
|
|
|
plot_fits = self.curve_checkbox.isChecked()
|
|
|
|
parts = self.partial_checkBox.checkState() == QtCore.Qt.Checked
|
|
|
|
extrapolate = [None, None, None]
|
|
error = []
|
|
if self.extrapolate_box.isChecked():
|
|
try:
|
|
extrapolate[0] = float(self.minx_line.text())
|
|
except (TypeError, ValueError):
|
|
error.append('Start value is missing')
|
|
try:
|
|
extrapolate[1] = float(self.maxx_line.text())
|
|
except (TypeError, ValueError):
|
|
error.append('End value is missing')
|
|
try:
|
|
extrapolate[2] = int(self.numx_line.text())
|
|
except (TypeError, ValueError):
|
|
error.append('Number of points is missing')
|
|
|
|
if error:
|
|
msg = QtWidgets.QMessageBox.warning(self, 'Error', 'Extrapolation failed because:\n' + '\n'.join(error))
|
|
return
|
|
else:
|
|
self.closed.emit(self._results, self._opts, graph, plot_fits, parts, extrapolate)
|
|
self.accept()
|
|
|
|
else:
|
|
self.reject()
|
|
|
|
|
|
class FitExtension(QtWidgets.QDialog):
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent=parent)
|
|
gridLayout = QtWidgets.QGridLayout(self)
|
|
|
|
self.label = QtWidgets.QLabel('Minimum value')
|
|
gridLayout.addWidget(self.label, 0, 0, 1, 1)
|
|
|
|
self.min_line = QtWidgets.QLineEdit()
|
|
self.min_line.setValidator(QtGui.QDoubleValidator())
|
|
gridLayout.addWidget(self.min_line, 0, 1, 1, 1)
|
|
|
|
self.label_2 = QtWidgets.QLabel('Maximum value')
|
|
gridLayout.addWidget(self.label_2, 1, 0, 1, 1)
|
|
|
|
self.max_line = QtWidgets.QLineEdit()
|
|
self.max_line.setValidator(QtGui.QDoubleValidator())
|
|
gridLayout.addWidget(self.max_line, 1, 1, 1, 1)
|
|
|
|
self.label_3 = QtWidgets.QLabel('Number of pts.')
|
|
gridLayout.addWidget(self.label_3, 2, 0, 1, 1)
|
|
|
|
self.num_pts = QtWidgets.QLineEdit()
|
|
self.num_pts.setValidator(QtGui.QIntValidator())
|
|
gridLayout.addWidget(self.num_pts, 2, 1, 1, 1)
|
|
|
|
self.buttonBox = QtWidgets.QDialogButtonBox()
|
|
self.buttonBox.setOrientation(QtCore.Qt.Horizontal)
|
|
self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Cancel | QtWidgets.QDialogButtonBox.Ok)
|
|
gridLayout.addWidget(self.buttonBox, 3, 0, 1, 2)
|
|
|
|
self.setLayout(gridLayout)
|
|
|
|
self.buttonBox.accepted.connect(self.accept)
|
|
self.buttonBox.rejected.connect(self.reject)
|
|
|
|
@property
|
|
def values(self):
|
|
try:
|
|
xmin = float(self.min_line.text())
|
|
xmax = float(self.max_line.text())
|
|
nums = int(self.num_pts.text())
|
|
except TypeError:
|
|
return None
|
|
|
|
return xmin, xmax, nums
|