2022-03-28 14:26:10 +00:00
|
|
|
from __future__ import annotations
|
|
|
|
|
2022-10-20 15:23:15 +00:00
|
|
|
from nmreval.utils.text import convert
|
2022-03-08 09:27:40 +00:00
|
|
|
|
|
|
|
from ..Qt import QtCore, QtWidgets, QtGui
|
|
|
|
from .._py.fitmodelwidget import Ui_FitParameter
|
|
|
|
from .._py.save_fitmodel_dialog import Ui_SaveDialog
|
|
|
|
from ..lib import get_icon
|
|
|
|
|
|
|
|
|
|
|
|
class FitModelWidget(QtWidgets.QWidget, Ui_FitParameter):
|
|
|
|
value_requested = QtCore.pyqtSignal(object)
|
|
|
|
value_changed = QtCore.pyqtSignal(str)
|
|
|
|
state_changed = QtCore.pyqtSignal()
|
|
|
|
|
|
|
|
def __init__(self, label: str = 'Fitparameter', parent=None, fixed: bool = False):
|
|
|
|
super().__init__(parent)
|
|
|
|
self.setupUi(self)
|
|
|
|
|
|
|
|
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.lineEdit.setMaximumWidth(60)
|
|
|
|
self.lineEdit_2.setMaximumWidth(60)
|
|
|
|
|
|
|
|
self.label_3.setText(f'< {label} <')
|
|
|
|
|
|
|
|
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.editingFinished.connect(lambda: self.value_changed.emit(self.parameter_line.text()))
|
|
|
|
self.fixed_check.toggled.connect(self.set_fixed)
|
|
|
|
|
|
|
|
if fixed:
|
|
|
|
self.fixed_check.hide()
|
|
|
|
|
|
|
|
self.menu = QtWidgets.QMenu(self)
|
|
|
|
self.add_links()
|
|
|
|
|
|
|
|
self.is_linked = None
|
|
|
|
self.parameter_pos = None
|
|
|
|
self.func_idx = None
|
|
|
|
|
|
|
|
self._linetext = '1'
|
|
|
|
|
|
|
|
@property
|
|
|
|
def name(self):
|
|
|
|
return convert(self.parametername.text().strip(), old='html', new='str')
|
|
|
|
|
|
|
|
def set_parameter_string(self, p: str):
|
2022-03-28 14:26:10 +00:00
|
|
|
self.parameter_line.setText(p)
|
|
|
|
self.parameter_line.setToolTip(p)
|
2022-03-08 09:27:40 +00:00
|
|
|
|
|
|
|
def set_bounds(self, lb: float, ub: float, cbox: bool = True):
|
|
|
|
self.checkBox.setCheckState(QtCore.Qt.Checked if cbox else QtCore.Qt.Unchecked)
|
|
|
|
for val, bds_line in [(lb, self.lineEdit), (ub, self.lineEdit_2)]:
|
|
|
|
if val is not None:
|
|
|
|
bds_line.setText(str(val))
|
|
|
|
else:
|
|
|
|
bds_line.setText('')
|
|
|
|
|
|
|
|
def enableBounds(self, value: int):
|
|
|
|
self.lineEdit.setEnabled(value == 2)
|
|
|
|
self.lineEdit_2.setEnabled(value == 2)
|
|
|
|
|
2022-10-20 15:23:15 +00:00
|
|
|
def set_parameter(self, p: float | None, bds: tuple[float, float, bool] = None,
|
2022-03-28 14:26:10 +00:00
|
|
|
fixed: bool = None, glob: bool = None):
|
2022-03-08 09:27:40 +00:00
|
|
|
if p is None:
|
|
|
|
# bad hack: linked parameter return (None, linked parameter)
|
|
|
|
# if p is None -> parameter is linked to argument given by bds
|
|
|
|
self.link_parameter(linkto=bds)
|
|
|
|
else:
|
2022-03-28 14:26:10 +00:00
|
|
|
ptext = f'{p:.4g}'
|
2022-03-08 09:27:40 +00:00
|
|
|
|
|
|
|
self.set_parameter_string(ptext)
|
|
|
|
|
2022-03-28 14:26:10 +00:00
|
|
|
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)
|
2022-03-08 09:27:40 +00:00
|
|
|
|
2022-03-28 14:26:10 +00:00
|
|
|
if glob is not None:
|
|
|
|
self.global_checkbox.setCheckState(QtCore.Qt.Checked if glob else QtCore.Qt.Unchecked)
|
2022-03-08 09:27:40 +00:00
|
|
|
|
|
|
|
def get_parameter(self):
|
|
|
|
if self.is_linked:
|
|
|
|
try:
|
|
|
|
p = float(self._linetext)
|
|
|
|
except ValueError:
|
|
|
|
p = 1.0
|
|
|
|
else:
|
|
|
|
try:
|
|
|
|
p = float(self.parameter_line.text().replace(',', '.'))
|
|
|
|
except ValueError:
|
|
|
|
_ = QtWidgets.QMessageBox().warning(self, 'Invalid value',
|
|
|
|
f'{self.parametername.text()} contains invalid values',
|
|
|
|
QtWidgets.QMessageBox.Cancel)
|
|
|
|
return None
|
|
|
|
|
|
|
|
if self.checkBox.isChecked():
|
|
|
|
try:
|
|
|
|
lb = float(self.lineEdit.text().replace(',', '.'))
|
|
|
|
except ValueError:
|
|
|
|
lb = None
|
|
|
|
|
|
|
|
try:
|
|
|
|
rb = float(self.lineEdit_2.text().replace(',', '.'))
|
|
|
|
except ValueError:
|
|
|
|
rb = None
|
|
|
|
else:
|
|
|
|
lb = rb = None
|
|
|
|
|
|
|
|
bounds = (lb, rb)
|
|
|
|
|
|
|
|
return p, bounds, not self.fixed_check.isChecked(), self.global_checkbox.isChecked(), self.is_linked
|
|
|
|
|
|
|
|
@QtCore.pyqtSlot(bool)
|
|
|
|
def set_fixed(self, state: bool):
|
|
|
|
# self.global_checkbox.setVisible(not state)
|
|
|
|
self.frame.setVisible(not state)
|
|
|
|
|
|
|
|
def add_links(self, parameter: dict = None):
|
|
|
|
if parameter is None:
|
|
|
|
parameter = {}
|
|
|
|
self.menu.clear()
|
|
|
|
|
|
|
|
ac = QtWidgets.QAction('Link to...', self)
|
|
|
|
ac.triggered.connect(self.link_parameter)
|
|
|
|
self.menu.addAction(ac)
|
|
|
|
|
|
|
|
for model_key, model_funcs in parameter.items():
|
|
|
|
m = QtWidgets.QMenu('Model ' + model_key, self)
|
|
|
|
for func_name, func_params in model_funcs.items():
|
|
|
|
m2 = QtWidgets.QMenu(func_name, m)
|
|
|
|
for p_name, idx in func_params:
|
|
|
|
ac = QtWidgets.QAction(p_name, m2)
|
|
|
|
ac.setData((model_key, *idx))
|
|
|
|
ac.triggered.connect(self.link_parameter)
|
|
|
|
m2.addAction(ac)
|
|
|
|
m.addMenu(m2)
|
|
|
|
self.menu.addMenu(m)
|
|
|
|
|
|
|
|
self.toolButton.setMenu(self.menu)
|
|
|
|
|
|
|
|
@QtCore.pyqtSlot()
|
|
|
|
def link_parameter(self, linkto=None):
|
|
|
|
if linkto is None:
|
|
|
|
action = self.sender()
|
|
|
|
else:
|
|
|
|
action = False
|
|
|
|
for m in self.menu.actions():
|
|
|
|
if m.menu():
|
|
|
|
for a in m.menu().actions():
|
|
|
|
if a.data() == linkto:
|
|
|
|
action = a
|
|
|
|
break
|
|
|
|
if action:
|
|
|
|
break
|
|
|
|
|
|
|
|
if (self.func_idx, self.parameter_pos) == action.data():
|
|
|
|
return
|
|
|
|
|
|
|
|
try:
|
|
|
|
new_text = f'Linked to {action.parentWidget().title()}.{action.text()}'
|
|
|
|
self._linetext = self.parameter_line.text()
|
|
|
|
self.parameter_line.setText(new_text)
|
|
|
|
self.parameter_line.setEnabled(False)
|
|
|
|
self.global_checkbox.hide()
|
|
|
|
self.global_checkbox.blockSignals(True)
|
|
|
|
self.global_checkbox.setCheckState(QtCore.Qt.Checked)
|
|
|
|
self.global_checkbox.blockSignals(False)
|
|
|
|
self.frame.hide()
|
|
|
|
self.is_linked = action.data()
|
|
|
|
|
|
|
|
except AttributeError:
|
|
|
|
self.parameter_line.setText(self._linetext)
|
|
|
|
self.parameter_line.setEnabled(True)
|
|
|
|
if self.fixed_check.isEnabled():
|
|
|
|
self.global_checkbox.show()
|
|
|
|
self.frame.show()
|
|
|
|
self.is_linked = None
|
|
|
|
|
|
|
|
self.state_changed.emit()
|
|
|
|
|
|
|
|
|
|
|
|
class QSaveModelDialog(QtWidgets.QDialog, Ui_SaveDialog):
|
|
|
|
def __init__(self, types=None, parent=None):
|
|
|
|
super().__init__(parent=parent)
|
|
|
|
self.setupUi(self)
|
|
|
|
|
|
|
|
if types is None:
|
|
|
|
types = []
|
|
|
|
|
|
|
|
self.comboBox.blockSignals(True)
|
|
|
|
self.comboBox.addItems(types)
|
|
|
|
self.comboBox.addItem('New group...')
|
|
|
|
self.comboBox.blockSignals(False)
|
|
|
|
|
|
|
|
self.frame.hide()
|
|
|
|
|
|
|
|
@QtCore.pyqtSlot(int, name='on_comboBox_currentIndexChanged')
|
|
|
|
def new_group(self, idx: int):
|
|
|
|
if idx == self.comboBox.count() - 1:
|
|
|
|
self.frame.show()
|
|
|
|
else:
|
|
|
|
self.lineEdit_2.clear()
|
|
|
|
self.frame.hide()
|
|
|
|
|
|
|
|
@QtCore.pyqtSlot(name='on_toolButton_clicked')
|
|
|
|
def accept_group(self):
|
|
|
|
self.comboBox.insertItem(self.comboBox.count() - 1, self.lineEdit_2.text())
|
|
|
|
self.comboBox.setCurrentIndex(self.comboBox.count() - 2)
|
|
|
|
|
|
|
|
def accept(self):
|
|
|
|
if self.lineEdit.text():
|
|
|
|
self.close()
|
|
|
|
|
|
|
|
|
|
|
|
class FitModelTree(QtWidgets.QTreeWidget):
|
|
|
|
icons = ['plus', 'mal_icon', 'minus_icon', 'geteilt_icon']
|
|
|
|
|
|
|
|
treeChanged = QtCore.pyqtSignal()
|
|
|
|
itemRemoved = QtCore.pyqtSignal(int)
|
|
|
|
|
|
|
|
counterRole = QtCore.Qt.UserRole + 1
|
|
|
|
operatorRole = QtCore.Qt.UserRole + 2
|
|
|
|
|
|
|
|
def __init__(self, parent=None):
|
|
|
|
super().__init__(parent=parent)
|
|
|
|
self.setHeaderHidden(True)
|
|
|
|
self.setDragEnabled(True)
|
|
|
|
self.setDragDropMode(QtWidgets.QTreeWidget.InternalMove)
|
|
|
|
self.setDefaultDropAction(QtCore.Qt.MoveAction)
|
|
|
|
|
|
|
|
self.itemSelectionChanged.connect(lambda: self.treeChanged.emit())
|
|
|
|
|
|
|
|
def keyPressEvent(self, evt):
|
|
|
|
operators = [QtCore.Qt.Key_Plus, QtCore.Qt.Key_Asterisk,
|
|
|
|
QtCore.Qt.Key_Minus, QtCore.Qt.Key_Slash]
|
|
|
|
|
|
|
|
if evt.key() == QtCore.Qt.Key_Delete:
|
|
|
|
for item in self.selectedItems():
|
|
|
|
self.remove_function(item)
|
|
|
|
|
|
|
|
elif evt.key() == QtCore.Qt.Key_Space:
|
|
|
|
for item in self.treeWidget.selectedItems():
|
|
|
|
item.setCheckState(0, QtCore.Qt.Checked) if item.checkState(
|
|
|
|
0) == QtCore.Qt.Unchecked else item.setCheckState(0, QtCore.Qt.Unchecked)
|
|
|
|
|
|
|
|
elif evt.key() in operators:
|
|
|
|
idx = operators.index(evt.key())
|
|
|
|
for item in self.selectedItems():
|
|
|
|
item.setData(0, self.operatorRole, idx)
|
|
|
|
item.setIcon(0, get_icon(self.icons[idx]))
|
|
|
|
|
|
|
|
else:
|
|
|
|
super().keyPressEvent(evt)
|
|
|
|
|
|
|
|
def dropEvent(self, evt: QtGui.QDropEvent):
|
|
|
|
super().dropEvent(evt)
|
|
|
|
self.treeChanged.emit()
|
|
|
|
|
|
|
|
def remove_function(self, item: QtWidgets.QTreeWidgetItem):
|
|
|
|
"""
|
|
|
|
Remove function and children from tree and dictionary
|
|
|
|
"""
|
|
|
|
while item.childCount():
|
|
|
|
self.remove_function(item.child(0))
|
|
|
|
|
|
|
|
if item.parent():
|
|
|
|
item.parent().removeChild(item)
|
|
|
|
else:
|
|
|
|
self.invisibleRootItem().removeChild(item)
|
|
|
|
|
|
|
|
idx = item.data(0, self.counterRole)
|
|
|
|
self.itemRemoved.emit(idx)
|
|
|
|
|
2022-10-20 15:23:15 +00:00
|
|
|
def add_function(self, idx: int, cnt: int, op: int, name: str, color: QtGui.QColor | str | tuple,
|
2022-03-08 09:27:40 +00:00
|
|
|
parent: QtWidgets.QTreeWidgetItem = None, children: list = None, active: bool = True, **kwargs):
|
|
|
|
"""
|
|
|
|
Add function to tree and dictionary of functions.
|
|
|
|
"""
|
|
|
|
if not isinstance(color, QtGui.QColor):
|
|
|
|
if isinstance(color, tuple):
|
|
|
|
color = QtGui.QColor.fromRgbF(*color)
|
|
|
|
else:
|
|
|
|
color = QtGui.QColor(color)
|
|
|
|
|
|
|
|
it = QtWidgets.QTreeWidgetItem()
|
|
|
|
it.setData(0, QtCore.Qt.UserRole, idx)
|
|
|
|
it.setData(0, self.counterRole, cnt)
|
|
|
|
it.setData(0, self.operatorRole, op)
|
|
|
|
it.setText(0, name)
|
|
|
|
it.setForeground(0, QtGui.QBrush(color))
|
|
|
|
|
|
|
|
it.setIcon(0, get_icon(self.icons[op]))
|
|
|
|
it.setCheckState(0, QtCore.Qt.Checked if active else QtCore.Qt.Unchecked)
|
|
|
|
|
|
|
|
if parent is None:
|
|
|
|
self.addTopLevelItem(it)
|
|
|
|
else:
|
|
|
|
parent.addChild(it)
|
|
|
|
|
|
|
|
if children is not None:
|
|
|
|
for c in children:
|
|
|
|
self.add_function(**c, parent=it)
|
|
|
|
|
|
|
|
self.setCurrentIndex(self.indexFromItem(it, 0))
|
|
|
|
|
|
|
|
def sizeHint(self):
|
|
|
|
w = super().sizeHint().width()
|
|
|
|
return QtCore.QSize(w, 100)
|
|
|
|
|
|
|
|
def get_selected(self):
|
|
|
|
try:
|
|
|
|
it = self.selectedItems()[0]
|
|
|
|
function_nr = it.data(0, QtCore.Qt.UserRole)
|
|
|
|
idx = it.data(0, self.counterRole)
|
|
|
|
|
|
|
|
except IndexError:
|
|
|
|
function_nr = None
|
|
|
|
idx = None
|
|
|
|
|
|
|
|
return function_nr, idx
|
|
|
|
|
2022-03-28 14:26:10 +00:00
|
|
|
def get_functions(self, full: bool = True, pos: int = -1, return_pos: bool = False, parent=None):
|
2022-03-08 09:27:40 +00:00
|
|
|
"""
|
|
|
|
Create nested list of functions in tree. Parameters saved are idx (Index of function in list of all functions),
|
|
|
|
cnt (counter of number to associate with functione values), ops (+, -, *, /), and maybe children.
|
|
|
|
"""
|
|
|
|
if parent is None:
|
|
|
|
parent = self.invisibleRootItem()
|
|
|
|
|
|
|
|
funcs = []
|
|
|
|
for i in range(parent.childCount()):
|
|
|
|
pos += 1
|
|
|
|
it = parent.child(i)
|
|
|
|
|
|
|
|
child = {
|
|
|
|
'idx': it.data(0, QtCore.Qt.UserRole),
|
|
|
|
'op': it.data(0, self.operatorRole),
|
|
|
|
'pos': pos,
|
|
|
|
'active': (it.checkState(0) == QtCore.Qt.Checked),
|
|
|
|
'children': []
|
|
|
|
}
|
|
|
|
|
|
|
|
if full:
|
|
|
|
child['name'] = it.text(0)
|
|
|
|
child['cnt'] = it.data(0, self.counterRole)
|
|
|
|
child['color'] = it.foreground(0).color().getRgbF()
|
|
|
|
|
|
|
|
if it.childCount():
|
|
|
|
child['children'], pos = self.get_functions(full=full, parent=it, pos=pos, return_pos=True)
|
|
|
|
|
|
|
|
funcs.append(child)
|
|
|
|
|
|
|
|
if return_pos:
|
|
|
|
return funcs, pos
|
|
|
|
else:
|
|
|
|
return funcs
|
|
|
|
|
|
|
|
|
|
|
|
class FitTableWidget(QtWidgets.QTableWidget):
|
|
|
|
def __init__(self, parent=None):
|
|
|
|
super().__init__(parent=parent)
|
|
|
|
|
|
|
|
self.horizontalHeader().hide()
|
|
|
|
self.verticalHeader().hide()
|
|
|
|
self.setColumnCount(2)
|
|
|
|
self.setSelectionBehavior(QtWidgets.QTableWidget.SelectRows)
|
|
|
|
self.horizontalHeader().setStretchLastSection(True)
|
|
|
|
self.hideColumn(1)
|
|
|
|
|
|
|
|
def add_model(self, idx: str):
|
|
|
|
model_count = 0
|
|
|
|
for r in range(self.rowCount()):
|
|
|
|
cb = self.cellWidget(r, 1)
|
|
|
|
cb.addItem('Model ' + str(idx), userData=idx)
|
|
|
|
model_count = cb.count()
|
|
|
|
|
|
|
|
if model_count > 2:
|
|
|
|
if self.isColumnHidden(1):
|
|
|
|
self.showColumn(1)
|
|
|
|
self.resizeColumnToContents(0)
|
|
|
|
self.setColumnWidth(1, self.columnWidth(0) - self.columnWidth(1))
|
|
|
|
|
|
|
|
def remove_model(self, idx: str):
|
|
|
|
model_count = 0
|
|
|
|
for r in range(self.rowCount()):
|
|
|
|
cb = self.cellWidget(r, 1)
|
|
|
|
if cb.currentData() == idx:
|
|
|
|
cb.setCurrentIndex(0)
|
|
|
|
cb.removeItem(cb.findData(idx))
|
|
|
|
model_count = cb.count()
|
|
|
|
|
|
|
|
if model_count == 2:
|
|
|
|
self.hideColumn(1)
|
|
|
|
self.resizeColumnToContents(0)
|
|
|
|
|
2022-10-20 15:23:15 +00:00
|
|
|
def load(self, set_ids: list[str]):
|
2022-03-08 09:27:40 +00:00
|
|
|
self.blockSignals(True)
|
|
|
|
|
|
|
|
while self.rowCount():
|
|
|
|
self.removeRow(0)
|
|
|
|
|
2022-04-12 16:45:30 +00:00
|
|
|
self.setColumnCount(2)
|
|
|
|
self.hideColumn(1)
|
|
|
|
|
2022-03-08 09:27:40 +00:00
|
|
|
for (sid, name) in set_ids:
|
|
|
|
item = QtWidgets.QTableWidgetItem(name)
|
|
|
|
item.setCheckState(QtCore.Qt.Checked)
|
|
|
|
item.setData(QtCore.Qt.UserRole+1, sid)
|
|
|
|
row = self.rowCount()
|
|
|
|
self.setRowCount(row+1)
|
|
|
|
self.setItem(row, 0, item)
|
|
|
|
|
|
|
|
item2 = QtWidgets.QTableWidgetItem('')
|
|
|
|
self.setItem(row, 1, item2)
|
2022-04-12 16:45:30 +00:00
|
|
|
cb = QtWidgets.QComboBox(parent=self)
|
2022-03-08 09:27:40 +00:00
|
|
|
cb.addItem('Default')
|
|
|
|
self.setCellWidget(row, 1, cb)
|
|
|
|
|
|
|
|
self.blockSignals(False)
|
|
|
|
|
2022-03-28 14:26:10 +00:00
|
|
|
def collect_data(self, default: str = None, include_name: bool = False) -> dict:
|
2022-03-08 09:27:40 +00:00
|
|
|
data = {}
|
|
|
|
|
|
|
|
for i in range(self.rowCount()):
|
|
|
|
item = self.item(i, 0)
|
|
|
|
if item.checkState() == QtCore.Qt.Checked:
|
|
|
|
mod = self.cellWidget(i, 1).currentData()
|
|
|
|
if mod is None:
|
|
|
|
mod = default
|
|
|
|
|
|
|
|
if include_name:
|
|
|
|
arg = (item.data(QtCore.Qt.UserRole+1), item.text())
|
|
|
|
else:
|
|
|
|
arg = item.data(QtCore.Qt.UserRole+1)
|
|
|
|
|
|
|
|
if mod not in data:
|
|
|
|
data[mod] = []
|
|
|
|
data[mod].append(arg)
|
|
|
|
|
|
|
|
return data
|
|
|
|
|
|
|
|
def data_list(self, include_name: bool = True) -> list:
|
|
|
|
ret_val = []
|
|
|
|
for i in range(self.rowCount()):
|
|
|
|
item = self.item(i, 0)
|
|
|
|
if include_name:
|
|
|
|
ret_val.append((item.data(QtCore.Qt.UserRole+1), item.text()))
|
|
|
|
else:
|
|
|
|
ret_val.append(item.data(QtCore.Qt.UserRole+1))
|
|
|
|
|
|
|
|
return ret_val
|