1
0
forked from IPKM/nmreval

BUGFIX: VFT;

change to src layout
This commit is contained in:
dominik
2022-10-20 17:23:15 +02:00
parent 89ce4bab9f
commit 8d148b639b
445 changed files with 1387 additions and 1920 deletions

View File

460
src/gui_qt/fit/fit_forms.py Normal file
View File

@ -0,0 +1,460 @@
from __future__ import annotations
from nmreval.utils.text import convert
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):
self.parameter_line.setText(p)
self.parameter_line.setToolTip(p)
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)
def set_parameter(self, p: float | None, bds: tuple[float, float, bool] = None,
fixed: bool = None, glob: bool = None):
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:
ptext = f'{p:.4g}'
self.set_parameter_string(ptext)
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)
if glob is not None:
self.global_checkbox.setCheckState(QtCore.Qt.Checked if glob else QtCore.Qt.Unchecked)
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)
def add_function(self, idx: int, cnt: int, op: int, name: str, color: QtGui.QColor | str | tuple,
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
def get_functions(self, full: bool = True, pos: int = -1, return_pos: bool = False, parent=None):
"""
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)
def load(self, set_ids: list[str]):
self.blockSignals(True)
while self.rowCount():
self.removeRow(0)
self.setColumnCount(2)
self.hideColumn(1)
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)
cb = QtWidgets.QComboBox(parent=self)
cb.addItem('Default')
self.setCellWidget(row, 1, cb)
self.blockSignals(False)
def collect_data(self, default: str = None, include_name: bool = False) -> dict:
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

View File

@ -0,0 +1,327 @@
from __future__ import annotations
from nmreval.utils.text import convert
from ..Qt import QtWidgets, QtCore, QtGui
from .._py.fitfuncwidget import Ui_FormFit
from ..lib.forms import SelectionWidget
from .fit_forms import FitModelWidget
class QFitParameterWidget(QtWidgets.QWidget, Ui_FormFit):
value_requested = QtCore.pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setupUi(self)
self.func = None
self.func_idx = None
self.max_width = QtCore.QSize(0, 0)
self.global_parameter = []
self.data_parameter = []
self.glob_values = None
self.data_values = {}
self.scrollwidget.setLayout(QtWidgets.QVBoxLayout())
self.scrollwidget2.setLayout(QtWidgets.QVBoxLayout())
def eventFilter(self, src: QtCore.QObject, evt: QtCore.QEvent):
if isinstance(evt, QtGui.QKeyEvent):
if (evt.key() == QtCore.Qt.Key_Right) and \
(evt.modifiers() == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier):
self.change_single_parameter(src.value, sender=src)
self.select_next_preview(1)
return True
elif (evt.key() == QtCore.Qt.Key_Left) and \
(evt.modifiers() == QtCore.Qt.ControlModifier | QtCore.Qt.ShiftModifier):
self.change_single_parameter(src.value, sender=src)
self.select_next_preview(-1)
return True
return super().eventFilter(src, evt)
def load(self, data):
self.comboBox.blockSignals(True)
while self.comboBox.count():
self.comboBox.removeItem(0)
for sid, name in data:
self.comboBox.addItem(name, userData=sid)
self._make_parameter(sid)
self.comboBox.blockSignals(False)
self.change_data(0)
def set_function(self, func, idx):
self.func = func
self.func_idx = idx
self.glob_values = [1] * len(func.params)
for k, v in enumerate(func.params):
name = convert(v)
widgt = FitModelWidget(label=name, parent=self.scrollwidget)
widgt.parameter_pos = k
widgt.func_idx = idx
try:
widgt.set_bounds(*func.bounds[k], False)
except (AttributeError, IndexError):
pass
size = widgt.parametername.sizeHint()
if self.max_width.width() < size.width():
self.max_width = size
widgt.state_changed.connect(self.make_global)
widgt.value_requested.connect(self.look_for_value)
widgt.value_changed.connect(self.change_global_parameter)
self.global_parameter.append(widgt)
self.scrollwidget.layout().addWidget(widgt)
widgt2 = ParameterSingleWidget(name=name, parent=self.scrollwidget2)
widgt2.valueChanged.connect(self.change_single_parameter)
widgt2.removeSingleValue.connect(self.change_single_parameter)
widgt2.installEventFilter(self)
self.scrollwidget2.layout().addWidget(widgt2)
self.data_parameter.append(widgt2)
for w1, w2 in zip(self.global_parameter, self.data_parameter):
w1.parametername.setFixedSize(self.max_width)
w1.checkBox.setFixedSize(self.max_width)
w2.label.setFixedSize(self.max_width)
if hasattr(func, 'choices') and func.choices is not None:
cbox = func.choices
for c in cbox:
widgt = SelectionWidget(*c)
widgt.selectionChanged.connect(self.change_global_choice)
self.global_parameter.append(widgt)
self.glob_values.append(widgt.value)
self.scrollwidget.layout().addWidget(widgt)
widgt2 = SelectionWidget(*c)
widgt2.selectionChanged.connect(self.change_single_choice)
self.data_parameter.append(widgt2)
self.scrollwidget2.layout().addWidget(widgt2)
for i in range(self.comboBox.count()):
self._make_parameter(self.comboBox.itemData(i))
self.scrollwidget.layout().addStretch(1)
self.scrollwidget2.layout().addStretch(1)
def set_links(self, parameter):
for w in self.global_parameter:
if isinstance(w, FitModelWidget):
w.add_links(parameter)
@QtCore.pyqtSlot(str)
def change_global_parameter(self, value: str, idx: int = None):
if idx is None:
idx = self.global_parameter.index(self.sender())
self.glob_values[idx] = float(value)
if self.data_values[self.comboBox.currentData()][idx] is None:
self.data_parameter[idx].blockSignals(True)
self.data_parameter[idx].value = float(value)
self.data_parameter[idx].blockSignals(False)
@QtCore.pyqtSlot(str, object)
def change_global_choice(self, _, value):
idx = self.global_parameter.index(self.sender())
self.glob_values[idx] = value
if self.data_values[self.comboBox.currentData()][idx] is None:
self.data_parameter[idx].blockSignals(True)
self.data_parameter[idx].value = value
self.data_parameter[idx].blockSignals(False)
def change_single_parameter(self, value: float = None, sender=None):
if sender is None:
sender = self.sender()
idx = self.data_parameter.index(sender)
self.data_values[self.comboBox.currentData()][idx] = value
# look for global parameter values if value is reset, ie None
if value is None:
self.change_data(self.comboBox.currentIndex())
def change_single_choice(self, _, value, sender=None):
if sender is None:
sender = self.sender()
idx = self.data_parameter.index(sender)
self.data_values[self.comboBox.currentData()][idx] = value
@QtCore.pyqtSlot(object)
def look_for_value(self, sender):
self.value_requested.emit(self.global_parameter.index(sender))
@QtCore.pyqtSlot()
def make_global(self):
# disable single parameter if it is set global, enable if global is unset
widget = self.sender()
idx = self.global_parameter.index(widget)
enable = (widget.global_checkbox.checkState() == QtCore.Qt.Unchecked) and (widget.is_linked is None)
self.data_parameter[idx].setEnabled(enable)
def select_next_preview(self, direction):
curr_idx = self.comboBox.currentIndex()
next_idx = (curr_idx + direction) % self.comboBox.count()
self.comboBox.setCurrentIndex(next_idx)
@QtCore.pyqtSlot(int, name='on_comboBox_currentIndexChanged')
def change_data(self, idx: int):
# new dataset is selected, look for locally set parameter else use global values
sid = self.comboBox.itemData(idx)
if sid not in self.data_values:
self._make_parameter(sid)
for i, value in enumerate(self.data_values[sid]):
w = self.data_parameter[i]
w.blockSignals(True)
if value is None:
w.value = self.glob_values[i]
else:
w.value = value
w.blockSignals(False)
def _make_parameter(self, sid):
if sid not in self.data_values:
self.data_values[sid] = [None] * len(self.data_parameter)
def get_parameter(self, use_func=None):
bds = []
is_global = []
is_fixed = []
globs = []
is_linked = []
for g in self.global_parameter:
if isinstance(g, FitModelWidget):
p_i, bds_i, fixed_i, global_i, link_i = g.get_parameter()
globs.append(p_i)
bds.append(bds_i)
is_fixed.append(fixed_i)
is_global.append(global_i)
is_linked.append(link_i)
lb, ub = list(zip(*bds))
data_parameter = {}
if use_func is None:
use_func = list(self.data_values.keys())
global_p = None
for sid, parameter in self.data_values.items():
if sid not in use_func:
continue
kw_p = {}
p = []
if global_p is None:
global_p = {'p': [], 'idx': [], 'var': [], 'ub': [], 'lb': []}
for i, (p_i, g) in enumerate(zip(parameter, self.global_parameter)):
if isinstance(g, FitModelWidget):
if (p_i is None) or is_global[i]:
p.append(globs[i])
if is_global[i]:
if i not in global_p['idx']:
global_p['p'].append(globs[i])
global_p['idx'].append(i)
global_p['var'].append(is_fixed[i])
global_p['ub'].append(ub[i])
global_p['lb'].append(lb[i])
else:
p.append(p_i)
try:
if p[i] > ub[i]:
raise ValueError(f'Parameter {g.name} is outside bounds ({lb[i]}, {ub[i]})')
except TypeError:
pass
try:
if p[i] < lb[i]:
raise ValueError(f'Parameter {g.name} is outside bounds ({lb[i]}, {ub[i]})')
except TypeError:
pass
else:
if p_i is None:
kw_p.update(g.value)
elif isinstance(p_i, dict):
kw_p.update(p_i)
else:
kw_p[g.argname] = p_i
data_parameter[sid] = (p, kw_p)
return data_parameter, lb, ub, is_fixed, global_p, is_linked
def set_parameter(self, set_id: str | None, parameter: list[float]) -> int:
if set_id is None:
for val, g in zip(parameter, self.global_parameter):
if isinstance(g, SelectionWidget):
continue
g.set_parameter(val)
else:
new_param = self.data_values[set_id]
min_len = min(len(new_param), len(parameter))
for i in range(min_len):
new_param[i] = parameter[i]
self.change_data(self.comboBox.currentIndex())
return len(self.global_parameter)
class ParameterSingleWidget(QtWidgets.QWidget):
valueChanged = QtCore.pyqtSignal(object)
removeSingleValue = QtCore.pyqtSignal()
def __init__(self, name: str, parent=None):
super().__init__(parent=parent)
self._init_ui()
self._name = name
self.label.setText(convert(name))
self.value_line.setValidator(QtGui.QDoubleValidator())
self.value_line.textChanged.connect(lambda: self.valueChanged.emit(self.value) if self.value is not None else 0)
self.reset_button.clicked.connect(lambda x: self.removeSingleValue.emit())
def _init_ui(self):
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(2, 2, 2, 2)
layout.setSpacing(2)
self.label = QtWidgets.QLabel(self)
layout.addWidget(self.label)
layout.addSpacerItem(QtWidgets.QSpacerItem(0, 0, QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Minimum))
self.value_line = QtWidgets.QLineEdit(self)
layout.addWidget(self.value_line)
self.reset_button = QtWidgets.QToolButton(self)
self.reset_button.setText('Use global')
layout.addWidget(self.reset_button)
self.setLayout(layout)
@property
def value(self) -> float:
try:
return float(self.value_line.text().replace(',', '.'))
except ValueError:
return 0.0
@value.setter
def value(self, val):
self.value_line.setText(f'{float(val):.5g}')

View File

@ -0,0 +1,213 @@
from __future__ import annotations
from itertools import cycle, count
from nmreval.configs import config_paths
from nmreval import models
from nmreval.lib.importer import find_models
from nmreval.lib.colors import BaseColor, Tab10
from nmreval.utils.text import convert
from ..lib import get_icon
from .._py.fitfunctionwidget import Ui_Form
from ..Qt import QtWidgets, QtCore, QtGui
class QFunctionWidget(QtWidgets.QWidget, Ui_Form):
func_cnt = count()
func_colors = cycle(Tab10)
op_names = ['plus_icon', 'mal_icon', 'minus_icon', 'geteilt_icon']
newFunction = QtCore.pyqtSignal(int, int)
treeChanged = QtCore.pyqtSignal()
itemRemoved = QtCore.pyqtSignal(int)
showFunction = QtCore.pyqtSignal(int)
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setupUi(self)
self._types = []
self.functions = find_models(models)
try:
self.functions += find_models(config_paths() / 'usermodels.py')
except FileNotFoundError:
pass
for m in self.functions:
try:
m.type
except AttributeError:
m.type = 'Other'
if m.type not in self._types:
self._types.append(m.type)
self.typecomboBox.addItems(sorted(self._types))
self.functree.treeChanged.connect(lambda: self.treeChanged.emit())
self.functree.itemRemoved.connect(self.remove_function)
self.iscomplex = False
self.complex_widget.hide()
for i, op_icon in enumerate(self.op_names):
self.operator_combobox.setItemIcon(i, get_icon(op_icon))
def __len__(self) -> int:
num = 0
iterator = QtWidgets.QTreeWidgetItemIterator(self.functree)
while iterator.value():
num += 1
iterator += 1
return num
@QtCore.pyqtSlot(int, name='on_typecomboBox_currentIndexChanged')
def change_group(self, idx: int):
"""
Change items in fitcombobox to new entries
"""
self.fitcomboBox.blockSignals(True)
while self.fitcomboBox.count():
self.fitcomboBox.removeItem(0)
selected_type = self.typecomboBox.itemText(idx)
for m in self.functions:
if m.type == selected_type:
self.fitcomboBox.addItem(m.name, userData=self.functions.index(m))
self.fitcomboBox.blockSignals(False)
self.on_fitcomboBox_currentIndexChanged(0)
@QtCore.pyqtSlot(int, name='on_fitcomboBox_currentIndexChanged')
def change_function(self, idx: int):
"""
Display new equation on changing function
"""
index = self.fitcomboBox.itemData(idx)
if self.functions:
fitfunc = self.functions[index]
try:
self.fitequation.setText(convert(fitfunc.equation))
except AttributeError:
self.fitequation.setText('')
@QtCore.pyqtSlot(name='on_use_function_button_clicked')
def new_function(self):
idx = self.fitcomboBox.itemData(self.fitcomboBox.currentIndex())
cnt = next(self.func_cnt)
op = self.operator_combobox.currentIndex()
name = self.functions[idx].name
col = next(self.func_colors)
self.newFunction.emit(idx, cnt)
self.add_function(idx, cnt, op, name, col)
def add_function(self, idx: int, cnt: int, op: int,
name: str, color: str | tuple[float, float, float] | BaseColor, **kwargs):
"""
Add function to tree and dictionary of functions.
"""
if isinstance(color, BaseColor):
qcolor = QtGui.QColor.fromRgbF(*color.rgb(normed=True))
elif isinstance(color, tuple):
qcolor = QtGui.QColor.fromRgbF(*color)
else:
qcolor = QtGui.QColor(color)
self.functree.add_function(idx, cnt, op, name, qcolor, **kwargs)
f = self.functions[idx]
if hasattr(f, 'iscomplex') and f.iscomplex:
self.iscomplex = True
self.complex_widget.show()
@QtCore.pyqtSlot(int)
def remove_function(self, idx: int):
iterator = QtWidgets.QTreeWidgetItemIterator(self.functree)
self.iscomplex = False
while iterator.value():
item = iterator.value()
f = self.functions[item.data(0, QtCore.Qt.UserRole)]
if hasattr(f, 'iscomplex') and f.iscomplex:
self.iscomplex = True
break
iterator += 1
self.complex_widget.setVisible(self.iscomplex)
self.itemRemoved.emit(idx)
@QtCore.pyqtSlot(QtWidgets.QTreeWidgetItem, int, name='on_functree_itemClicked')
def show_parameter(self, item: QtWidgets.QTreeWidgetItem, _: int):
self.showFunction.emit(item.data(0, self.functree.counterRole))
def get_selected(self):
function_nr, idx = self.functree.get_selected()
if function_nr is not None:
return self.functions[function_nr], idx
else:
return None, None
def get_functions(self, full: bool = True, clsname=False, include_all: bool = True):
"""
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.
"""
used_functions = self.functree.get_functions(full=full)
self._prepare_function_for_model(used_functions, full=full, clsname=clsname, include_all=include_all)
return used_functions
def _prepare_function_for_model(self, func_list: list[dict],
full: bool = True, clsname: bool = False, include_all: bool = True):
for func_args in func_list:
is_active = func_args.get('active')
if (not is_active) and (not include_all):
continue
if not clsname:
func_args['func'] = self.functions[func_args['idx']]
else:
func_args['func'] = self.functions[func_args['idx']].name
if not full:
func_args.pop('active')
func_args.pop('idx')
if func_args['children']:
self._prepare_function_for_model(func_args['children'],
full=full, clsname=clsname,
include_all=include_all)
def get_parameter_list(self):
all_parameters = {}
iterator = QtWidgets.QTreeWidgetItemIterator(self.functree)
while iterator.value():
item = iterator.value()
f = self.functions[item.data(0, QtCore.Qt.UserRole)]
cnt = item.data(0, self.functree.counterRole)
all_parameters[f'{f.name}_{cnt}'] = [(convert(pp, new='str'), (cnt, i)) for i, pp in enumerate(f.params)]
iterator += 1
return all_parameters
def get_complex_state(self):
return self.complex_comboBox.currentIndex() if self.iscomplex else None
def set_complex_state(self, state):
if state is not None:
self.complex_comboBox.setCurrentIndex(state)
def clear(self):
self.functree.blockSignals(True)
self.functree.clear()
self.functree.blockSignals(False)
self.complex_comboBox.setCurrentIndex(0)
self.complex_widget.hide()

474
src/gui_qt/fit/fitwindow.py Normal file
View File

@ -0,0 +1,474 @@
from __future__ import annotations
from functools import reduce
from itertools import count, cycle
from operator import add
from string import ascii_letters
from typing import Dict, List, Tuple
import numpy as np
from pyqtgraph import mkPen
from nmreval.fit._meta import MultiModel, ModelFactory
from nmreval.fit.result import FitResult
from .fit_forms import FitTableWidget
from .fit_parameter import QFitParameterWidget
from ..lib.pg_objects import PlotItem
from ..Qt import QtGui, QtCore, QtWidgets
from .._py.fitdialog import Ui_FitDialog
class QFitDialog(QtWidgets.QWidget, Ui_FitDialog):
func_cnt = count()
model_cnt = cycle(ascii_letters)
preview_num = 201
preview_emit = QtCore.pyqtSignal(dict, int, bool)
fitStartSig = QtCore.pyqtSignal(dict, list, dict)
abortFit = QtCore.pyqtSignal()
def __init__(self, mgmt=None, parent=None):
super().__init__(parent=parent)
self.setupUi(self)
self.parameters = {}
self.preview_lines = []
self._current_function = None
self.param_widgets = {}
self._management = mgmt
self._current_model = next(QFitDialog.model_cnt)
self.show_combobox.setItemData(0, self._current_model, QtCore.Qt.UserRole)
self.default_combobox.setItemData(0, self._current_model, QtCore.Qt.UserRole)
self.data_table = FitTableWidget(self.data_widget)
self.data_widget.addWidget(self.data_table)
self.data_widget.setText('Data')
self.models = {}
self._func_list = {}
self._complex = {}
self.connected_figure = ''
self.model_frame.hide()
self.preview_button.hide()
self.abort_button.clicked.connect(lambda: self.abortFit.emit())
self.functionwidget.newFunction.connect(self.add_function)
self.functionwidget.showFunction.connect(self.show_function_parameter)
self.functionwidget.itemRemoved.connect(self.remove_function)
@QtCore.pyqtSlot(int, int)
def add_function(self, function_idx: int, function_id: int):
self.show_function_parameter(function_id, function_idx)
self.newmodel_button.setEnabled(True)
@QtCore.pyqtSlot(int)
def remove_function(self, idx: int):
"""
Remove function and children from tree and dictionary
"""
w = self.param_widgets[idx]
self.stackedWidget.removeWidget(w)
w.deleteLater()
del self.param_widgets[idx]
if len(self.functionwidget) == 0:
# empty model
self.newmodel_button.setEnabled(False)
self.deletemodel_button.setEnabled(False)
self._current_function = None
else:
f_tree = self.functionwidget.functree
func_idx = f_tree.currentItem().data(0, f_tree.counterRole)
self._current_function = self.functionwidget.functions[func_idx]
@QtCore.pyqtSlot(int)
def show_function_parameter(self, function_id: int, function_idx: int = None):
"""
Display parameter associated with selected function.
"""
if function_id in self.param_widgets:
dialog = self.param_widgets[function_id]
else:
# create new widget for function
if function_idx is not None:
function = self.functionwidget.functions[function_idx]
else:
raise ValueError('No function index given')
if function is None:
return
dialog = QFitParameterWidget()
data_names = self.data_table.data_list(include_name=True)
dialog.set_function(function, function_idx)
dialog.load(data_names)
dialog.value_requested.connect(self.look_value)
self.stackedWidget.addWidget(dialog)
self.param_widgets[function_id] = dialog
self.stackedWidget.setCurrentWidget(dialog)
# collect parameter names etc. to allow linkage
self._func_list[self._current_model] = self.functionwidget.get_parameter_list()
dialog.set_links(self._func_list)
# show same tab (general parameter/Data parameter)
tab_idx = 0
if self._current_function is not None:
tab_idx = self.param_widgets[self._current_function].tabWidget.currentIndex()
dialog.tabWidget.setCurrentIndex(tab_idx)
self._current_function = function_id
def look_value(self, idx: int):
func_widget = self.param_widgets[self._current_function]
set_ids = [func_widget.comboBox.itemData(i) for i in range(func_widget.comboBox.count())]
for s in set_ids:
func_widget.data_values[s][idx] = self._management[s].value
func_widget.change_data(func_widget.comboBox.currentIndex())
def get_functions(self):
""" update functions, parameters"""
self.models[self._current_model] = self.functionwidget.get_functions()
self._complex[self._current_model] = self.functionwidget.get_complex_state()
self._func_list[self._current_model] = self.functionwidget.get_parameter_list()
def load(self, ids: List[str]):
"""
Add name and id of dataset to list.
"""
self.data_table.load(ids)
if self.models:
for m in self.models.keys():
self.data_table.add_model(m)
else:
self.data_table.add_model(self._current_model)
for dialog in self.param_widgets.values():
dialog.load(ids)
@QtCore.pyqtSlot(name='on_newmodel_button_clicked')
def make_new_model(self):
"""
Save model with all its functions in dictionary and adjust gui.
"""
self.deletemodel_button.setEnabled(True)
self.model_frame.show()
idx = next(QFitDialog.model_cnt)
self.data_table.add_model(idx)
self.default_combobox.addItem('Model '+idx, userData=idx)
self.show_combobox.addItem('Model '+idx, userData=idx)
self.show_combobox.setItemData(self.show_combobox.count()-1, idx, QtCore.Qt.UserRole)
self.show_combobox.setCurrentIndex(self.show_combobox.count()-1)
self._current_model = idx
self.stackedWidget.setCurrentIndex(0)
@QtCore.pyqtSlot(int, name='on_show_combobox_currentIndexChanged')
def change_model(self, idx: int):
"""
Save old model and display new model.
"""
self.get_functions()
self.functionwidget.clear()
self._current_model = self.show_combobox.itemData(idx, QtCore.Qt.UserRole)
if self._current_model in self.models and len(self.models[self._current_model]):
for el in self.models[self._current_model]:
self.functionwidget.add_function(**el)
self.functionwidget.set_complex_state(self._complex[self._current_model])
else:
self.stackedWidget.setCurrentIndex(0)
@QtCore.pyqtSlot(name='on_deletemodel_button_clicked')
def remove_model(self):
model_id = self._current_model
self.show_combobox.removeItem(self.show_combobox.findData(model_id))
self.default_combobox.removeItem(self.default_combobox.findData(model_id))
for m in self.models[model_id]:
func_id = m['cnt']
self.stackedWidget.removeWidget(self.param_widgets[func_id])
self.param_widgets.pop(func_id)
self._complex.pop(model_id)
self._func_list.pop(model_id)
self.models.pop(model_id)
self.data_table.remove_model(model_id)
if len(self.models) == 1:
self.model_frame.hide()
def _prepare(self, model: list, function_use: list = None,
parameter: dict = None, add_idx: bool = False, cnt: int = 0) -> Tuple[dict, int]:
if parameter is None:
parameter = {'parameter': {}, 'lb': (), 'ub': (), 'var': [],
'glob': {'idx': [], 'p': [], 'var': [], 'lb': [], 'ub': []},
'links': [], 'color': []}
for i, f in enumerate(model):
if not f['active']:
continue
try:
p, lb, ub, var, glob, links = self.param_widgets[f['cnt']].get_parameter(function_use)
except ValueError as e:
_ = QtWidgets.QMessageBox().warning(self, 'Invalid value', str(e),
QtWidgets.QMessageBox.Ok)
return {}, -1
p_len = len(parameter['lb'])
parameter['lb'] += lb
parameter['ub'] += ub
parameter['var'] += var
parameter['links'] += links
parameter['color'] += [f['color']]
for p_k, v_k in p.items():
if add_idx:
kw_k = {f'{k}_{cnt}': v for k, v in v_k[1].items()}
else:
kw_k = v_k[1]
if p_k in parameter['parameter']:
params, kw = parameter['parameter'][p_k]
params += v_k[0]
kw.update(kw_k)
else:
parameter['parameter'][p_k] = (v_k[0], kw_k)
for g_k, g_v in glob.items():
if g_k != 'idx':
parameter['glob'][g_k] += g_v
else:
parameter['glob']['idx'] += [idx_i + p_len for idx_i in g_v]
if add_idx:
cnt += 1
if f['children']:
# recurse for children
child_parameter, cnt = self._prepare(f['children'], parameter=parameter, add_idx=add_idx, cnt=cnt)
return parameter, cnt
@QtCore.pyqtSlot(name='on_fit_button_clicked')
def start_fit(self):
self.get_functions()
data = self.data_table.collect_data(default=self.default_combobox.currentData())
func_dict = {}
for k, mod in self.models.items():
func, order, param_len = ModelFactory.create_from_list(mod)
if func is None:
continue
if k in data:
parameter, _ = self._prepare(mod, function_use=data[k], add_idx=isinstance(func, MultiModel))
if parameter is None:
return
parameter['func'] = func
parameter['order'] = order
parameter['len'] = param_len
parameter['complex'] = self._complex[k]
if self._complex[k] is not None:
for p_k, p_v in parameter['parameter'].items():
p_v[1].update({'complex_mode': self._complex[k]})
parameter['parameter'][p_k] = p_v[0], p_v[1]
func_dict[k] = parameter
replaceable = []
for k, v in func_dict.items():
for i, link_i in enumerate(v['links']):
if link_i is None:
continue
rep_model, rep_func, rep_pos = link_i
try:
f = func_dict[rep_model]
except KeyError:
QtWidgets.QMessageBox().warning(self, 'Invalid value',
'Parameter cannot be linked: Model is unused',
QtWidgets.QMessageBox.Ok)
return
try:
f_idx = f['order'].index(rep_func)
except ValueError:
QtWidgets.QMessageBox().warning(self, 'Invalid value',
'Parameter cannot be linked: '
'Function is probably not checked or deleted',
QtWidgets.QMessageBox.Ok)
return
repl_idx = sum(f['len'][:f_idx])+rep_pos
if repl_idx not in f['glob']['idx']:
_ = QtWidgets.QMessageBox().warning(self, 'Invalid value',
'Parameter cannot be linked: '
'Destination is not a global parameter.',
QtWidgets.QMessageBox.Ok)
return
replaceable.append((k, i, rep_model, repl_idx))
replace_value = None
for p_k in f['parameter'].values():
replace_value = p_k[0][repl_idx]
break
if replace_value is not None:
for p_k in v['parameter'].values():
p_k[0][i] = replace_value
weight = ['None', 'y', 'y2', 'Deltay'][self.weight_combobox.currentIndex()]
fit_args = {'we': weight}
if func_dict:
self.fitStartSig.emit(func_dict, replaceable, fit_args)
return func_dict
@QtCore.pyqtSlot(int, name='on_preview_checkbox_stateChanged')
def show_preview(self, state: int):
if state:
self.preview_button.show()
self.preview_checkbox.setText('')
self._prepare_preview()
else:
self.preview_emit.emit({}, -1, False)
self.preview_lines = []
self.preview_button.hide()
self.preview_checkbox.setText('Preview')
@QtCore.pyqtSlot(name='on_preview_button_clicked')
def _prepare_preview(self):
self.get_functions()
default_model = self.default_combobox.currentData()
data = self.data_table.collect_data(default=default_model)
func_dict = {}
for k, mod in self.models.items():
func, order, param_len = ModelFactory.create_from_list(mod)
multiple_funcs = isinstance(func, MultiModel)
if k in data:
parameter, _ = self._prepare(mod, function_use=data[k], add_idx=multiple_funcs)
parameter['func'] = func
parameter['order'] = order
parameter['len'] = param_len
func_dict[k] = parameter
for v in func_dict.values():
for i, link_i in enumerate(v['links']):
if link_i is None:
continue
rep_model, rep_func, rep_pos = link_i
f = func_dict[rep_model]
f_idx = f['order'].index(rep_func)
repl_idx = sum(f['len'][:f_idx]) + rep_pos
replace_value = None
for p_k in f['parameter'].values():
replace_value = p_k[0][repl_idx]
break
if replace_value is not None:
for p_k in v['parameter'].values():
p_k[0][i] = replace_value
self.preview_emit.emit(func_dict, QFitDialog.preview_num, True)
def make_previews(self, x, models_parameters: dict):
self.preview_lines = []
for k, model in models_parameters.items():
f = model['func']
is_complex = self._complex[k]
parameters = model['parameter']
color = model['color']
for p, kwargs in parameters.values():
if is_complex is not None:
y = f.func(x, *p, complex_mode=is_complex, **kwargs)
if np.iscomplexobj(y):
self.preview_lines.append(PlotItem(x=x, y=y.real, pen=mkPen(width=3)))
self.preview_lines.append(PlotItem(x=x, y=y.imag, pen=mkPen(width=3)))
else:
self.preview_lines.append(PlotItem(x=x, y=y, pen=mkPen(width=3)))
else:
y = f.func(x, *p, **kwargs)
self.preview_lines.append(PlotItem(x=x, y=y, pen=mkPen(width=3)))
if isinstance(f, MultiModel):
sub_kwargs = kwargs.copy()
if is_complex is not None:
sub_kwargs.update({'complex_mode': is_complex})
for i, s in enumerate(f.subs(x, *p, **sub_kwargs)):
pen_i = mkPen(QtGui.QColor.fromRgbF(*color[i]))
if np.iscomplexobj(s):
self.preview_lines.append(PlotItem(x=x, y=s.real, pen=pen_i))
self.preview_lines.append(PlotItem(x=x, y=s.imag, pen=pen_i))
else:
self.preview_lines.append(PlotItem(x=x, y=s, pen=pen_i))
return self.preview_lines
def set_parameter(self, parameter: Dict[str, FitResult]):
# which data uses which model
data = self.data_table.collect_data(default=self.default_combobox.currentData())
glob_fit_parameter = []
for fitted_model, fitted_data in data.items():
for fit_id, fit_curve in parameter.items():
if fit_id in fitted_data:
fit_parameter = list(fit_curve.parameter.values())
glob_fit_parameter.append(fit_parameter)
self.set_parameter_iter(fit_id, [p.value for p in fit_parameter], self.models[fitted_model])
mean_parameter = [reduce(add, p, 0)/len(p) for p in zip(*glob_fit_parameter)]
self.set_parameter_iter(None, mean_parameter, self.models[fitted_model])
def set_parameter_iter(self, fit_id: str | None, param: List[float], functions: List, cnt: int = 0):
for model_p in functions:
if model_p['active']:
cnt += self.param_widgets[model_p['cnt']].set_parameter(fit_id, param[cnt:])
if model_p['children']:
cnt += self.set_parameter_iter(fit_id, param, model_p['children'], cnt=cnt)
return cnt
def closeEvent(self, evt: QtGui.QCloseEvent):
self.preview_emit.emit({}, -1, False)
self.preview_lines = []
super().closeEvent(evt)

View File

@ -0,0 +1,449 @@
from __future__ import annotations
import inspect
import numbers
import textwrap
from typing import Any
import numpy as np
from gui_qt.Qt import QtCore, QtWidgets, QtGui
from gui_qt._py.fitcreationdialog import Ui_Dialog
from gui_qt.lib.namespace import QNamespaceWidget
__all__ = ['QUserFitCreator']
validator = QtGui.QRegExpValidator(QtCore.QRegExp('[A-Za-z]\S*'))
class QUserFitCreator(QtWidgets.QDialog, Ui_Dialog):
classCreated = QtCore.pyqtSignal(object)
def __init__(self, parent=None):
super().__init__(parent=parent)
self.setupUi(self)
self.description_widget = DescWidget(self)
self.args_widget = ArgWidget(self)
self.kwargs_widget = KwargsWidget(self)
self.kwargs_widget.Changed.connect(self.update_function)
self.namespace_widget = QNamespaceWidget(self)
self.namespace_widget.make_namespace()
self.namespace_widget.sendKey.connect(self.namespace_made)
for b, w in [(self.description_box, self.description_widget), (self.args_box, self.args_widget),
(self.kwargs_box, self.kwargs_widget), (self.namespace_box, self.namespace_widget)]:
b.layout().addWidget(w)
try:
w.Changed.connect(self.update_function)
except AttributeError:
pass
b.layout().addStretch()
self._imports = set()
self.update_function()
def __call__(self, *args, **kwargs):
return self
def update_function(self):
try:
var = self.args_widget.get_parameter()
var += self.kwargs_widget.get_parameter()
k = ''
for imps in self._imports:
if len(imps) == 2:
k += f'from {imps[0]} import {imps[1]}\n'
elif imps[0] == 'numpy':
k += 'import numpy as np\n'
if len(self._imports):
k += '\n\n'
k += self.description_widget.get_strings()
k += self.args_widget.get_strings()
k += self.kwargs_widget.get_strings()
k += '\n @staticmethod\n'
k += f" def func(x, {', '.join(var)}):\n"
self.plainTextEdit.setPlainText(k)
except Exception as e:
QtWidgets.QMessageBox.warning(self, 'Failure', f'Error found: {e.args[0]}')
def change_visibility(self):
sender = self.sender()
for box in (self.description_box, self.args_box, self.kwargs_box, self.namespace_box):
box.blockSignals(True)
box.setExpansion(sender == box)
box.blockSignals(False)
def namespace_made(self, invalue: str):
ns = self.namespace_widget.namespace.namespace
func_value = ns[invalue][0]
ret_func = ''
if func_value is None:
ret_func = invalue
elif isinstance(func_value, numbers.Number):
ret_func = func_value
elif isinstance(func_value, np.ufunc):
self._imports.add(('numpy',))
ret_func = 'np.'+func_value.__name__ + '(x)'
else:
f_string = ns[invalue][-1]
args = f_string[f_string.find('('):]
if inspect.ismethod(func_value):
ret_func = func_value.__self__.__name__ + '.func'+args
elif hasattr(func_value, '__qualname__'):
ret_func = func_value.__qualname__.split('.')[0]
self._imports.add((inspect.getmodule(func_value).__name__, ret_func))
self.plainTextEdit.insertPlainText(ret_func)
self.update_function()
class KwargsWidget(QtWidgets.QWidget):
Changed = QtCore.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self._num_kwargs = 0
self._setup_ui()
def _setup_ui(self):
layout = QtWidgets.QGridLayout()
layout.setContentsMargins(3, 3, 3, 3)
layout.setHorizontalSpacing(3)
self.use_nuclei = QtWidgets.QCheckBox('Add gyromagnetic ratio', self)
self.use_nuclei.stateChanged.connect(lambda x: self.Changed.emit())
layout.addWidget(self.use_nuclei, 0, 0, 1, 3)
self.choices = QtWidgets.QTabWidget(self)
layout.addWidget(self.choices, 1, 0, 1, 3)
self.add_choice_button = QtWidgets.QPushButton('Add choice', self)
self.add_choice_button.clicked.connect(self.add_choice)
layout.addWidget(self.add_choice_button, 2, 0, 1, 1)
self.rem_choice_button = QtWidgets.QPushButton('Remove choice', self)
self.rem_choice_button.clicked.connect(self.remove_choice)
layout.addWidget(self.rem_choice_button, 2, 1, 1, 1)
self.setLayout(layout)
def add_choice(self):
cnt = self._num_kwargs
c = ChoiceWidget(cnt, self)
c.Changed.connect(self.update_choice)
self.choices.addTab(c, c.name_line.text())
self._num_kwargs += 1
self.choices.setCurrentIndex(cnt)
self.Changed.emit()
def remove_choice(self):
cnt = self.choices.currentIndex()
self.choices.removeTab(cnt)
self.Changed.emit()
def update_choice(self):
idx = self.choices.currentIndex()
self.choices.setTabText(idx, self.sender().name_line.text())
self.Changed.emit()
def get_parameter(self):
if self.use_nuclei.isChecked():
var = ['nucleus=2.67522128e7']
else:
var = []
var += [self.choices.widget(idx).get_parameter() for idx in range(self.choices.count())]
return var
def get_strings(self) -> str:
kwargs = []
if self.use_nuclei.isChecked():
kwargs.append("(r'\gamma', 'nucleus', gamma)")
for i in range(self.choices.count()):
kwargs.append(self.choices.widget(i).get_strings())
if kwargs:
return f" choices = {', '.join(kwargs)}\n"
else:
return ''
class ChoiceWidget(QtWidgets.QWidget):
Changed = QtCore.pyqtSignal()
def __init__(self, idx: int, parent=None):
super().__init__(parent=parent)
self._setup_ui()
self.name_line.setText('choice' + str(idx))
self.add_option()
def _setup_ui(self):
layout = QtWidgets.QGridLayout()
layout.setContentsMargins(3, 3, 3, 3)
layout.setHorizontalSpacing(3)
self.name_label = QtWidgets.QLabel('Name', self)
layout.addWidget(self.name_label, 0, 0, 1, 1)
self.name_line = QtWidgets.QLineEdit(self)
self.name_line.textChanged.connect(lambda x: self.Changed.emit())
layout.addWidget(self.name_line, 0, 1, 1, 1)
self.disp_label = QtWidgets.QLabel('Disp. name', self)
layout.addWidget(self.disp_label, 1, 0, 1, 1)
self.display_line = QtWidgets.QLineEdit(self)
self.display_line.textChanged.connect(lambda x: self.Changed.emit())
layout.addWidget(self.display_line, 1, 1, 1, 1)
self.add_button = QtWidgets.QPushButton('Add option', self)
self.add_button.clicked.connect(self.add_option)
layout.addWidget(self.add_button, 2, 0, 1, 2)
self.remove_button = QtWidgets.QPushButton('Remove option', self)
self.remove_button.clicked.connect(self.remove_option)
layout.addWidget(self.remove_button, 3, 0, 1, 2)
self.table = QtWidgets.QTableWidget(self)
self.table.setColumnCount(3)
self.table.setHorizontalHeaderLabels(['Name', 'Value', 'Type'])
self.table.itemChanged.connect(lambda x: self.Changed.emit())
layout.addWidget(self.table, 0, 2, 4, 1)
self.setLayout(layout)
def add_option(self):
self.table.blockSignals(True)
row = self.table.rowCount()
self.table.setRowCount(row+1)
self.table.setItem(row, 0, QtWidgets.QTableWidgetItem('opt' + str(row)))
lineedit = QtWidgets.QLineEdit()
lineedit.setValidator(validator)
lineedit.setFrame(False)
lineedit.setText('opt'+str(row))
lineedit.textChanged.connect(lambda x: self.Changed.emit())
self.table.setCellWidget(row, 0, lineedit)
self.table.setItem(row, 1, QtWidgets.QTableWidgetItem('None'))
self.table.setItem(row, 2, QtWidgets.QTableWidgetItem(''))
cb = QtWidgets.QComboBox()
cb.addItems(['None', 'str', 'float', 'int', 'bool'])
cb.currentIndexChanged.connect(lambda x: self.Changed.emit())
self.table.setCellWidget(row, 2, cb)
self.table.blockSignals(False)
self.Changed.emit()
def remove_option(self):
if self.table.rowCount() > 1:
self.table.blockSignals(True)
self.table.removeRow(self.table.currentRow())
self.table.blockSignals(False)
self.Changed.emit()
def get_parameter(self) -> str:
return f'{self.name_line.text()}={self._make_value(0)!r}'
def get_strings(self) -> str:
opts = []
for i in range(self.table.rowCount()):
name = self.table.item(i, 0).text()
val = self._make_value(i)
opts.append(f'{name!r}: {val!r}')
opts = f"{{{', '.join(opts)}}}"
disp = self.display_line.text()
name = self.name_line.text()
if disp == '':
ret_val = '(' + ', '.join([repr(name), repr(name), opts]) + ')'
else:
ret_val = '(' + ', '.join([repr(name), repr(disp), opts]) + ')'
return ret_val
def _make_value(self, i) -> Any:
dtype = self.table.cellWidget(i, 2).currentIndex()
val = self.table.item(i, 1).text()
cast = [None, str, float, int, bool]
if dtype == 0:
val = None
else:
try:
val = cast[dtype](val)
except:
raise ValueError(f'Invalid argument for {self.table.cellWidget(i, 0).text()}')
return val
class ArgWidget(QtWidgets.QWidget):
Changed = QtCore.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self._setup_ui()
def _setup_ui(self):
layout = QtWidgets.QGridLayout()
layout.setContentsMargins(3, 3, 3, 3)
layout.setHorizontalSpacing(3)
self.table = QtWidgets.QTableWidget(self)
self.table.setColumnCount(4)
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.table.setRowCount(0)
self.table.setHorizontalHeaderLabels(['Variable', 'Disp. name', 'Lower bound', 'Upper bound'])
self.table.itemChanged.connect(lambda x: self.Changed.emit())
layout.addWidget(self.table, 0, 0, 1, 3)
self.add_button = QtWidgets.QPushButton('Add parameter', self)
self.add_button.clicked.connect(self.add_variable)
layout.addWidget(self.add_button, 1, 0, 1, 1)
self.rem_button = QtWidgets.QPushButton('Remove parameter', self)
self.rem_button.clicked.connect(self.remove_variable)
layout.addWidget(self.rem_button, 1, 1, 1, 1)
spacer = QtWidgets.QSpacerItem(0, 0)
layout.addItem(spacer, 1, 2, 1, 1)
self.setLayout(layout)
def add_variable(self):
self.table.blockSignals(True)
row = self.table.rowCount()
self.table.setRowCount(row + 1)
self.table.setItem(row, 0, QtWidgets.QTableWidgetItem('p' + str(row)))
# arguments cannot start with a number or have spaces
lineedit = QtWidgets.QLineEdit()
lineedit.setValidator(validator)
lineedit.setFrame(False)
lineedit.setText('p'+str(row))
lineedit.textChanged.connect(lambda x: self.Changed.emit())
self.table.setCellWidget(row, 0, lineedit)
self.table.setItem(row, 1, QtWidgets.QTableWidgetItem('p_{' + str(row) + '}'))
self.table.setItem(row, 2, QtWidgets.QTableWidgetItem('--'))
self.table.setItem(row, 3, QtWidgets.QTableWidgetItem('--'))
self.table.blockSignals(False)
self.Changed.emit()
def remove_variable(self):
self.table.blockSignals(True)
self.table.removeRow(self.table.currentRow())
self.table.blockSignals(False)
self.Changed.emit()
def get_parameter(self) -> list[str]:
var = []
for row in range(self.table.rowCount()):
var.append(self.table.cellWidget(row, 0).text())
return var
def get_strings(self):
args = []
bnds = []
for row in range(self.table.rowCount()):
args.append(self.table.item(row, 1).text())
lb = self.table.item(row, 2).text()
lb = None if lb in ['--', 'None'] else float(lb)
ub = self.table.item(row, 3).text()
ub = None if ub in ['--', 'None'] else float(ub)
if ub is not None and lb is not None:
if not (lb < ub):
raise ValueError('Some bounds are invalid')
bnds.append(f'({lb}, {ub})')
stringi = f' params = {args}\n'
stringi += f" bounds = [{', '.join(bnds)}]\n"
return stringi
class DescWidget(QtWidgets.QWidget):
Changed = QtCore.pyqtSignal()
def __init__(self, parent=None):
super().__init__(parent=parent)
self._setup_ui()
def _setup_ui(self):
layout = QtWidgets.QGridLayout()
layout.setContentsMargins(3, 3, 3, 3)
layout.setSpacing(3)
self.klass_label = QtWidgets.QLabel('Class', self)
layout.addWidget(self.klass_label, 0, 0, 1, 1)
self.klass_lineedit = QtWidgets.QLineEdit(self)
self.klass_lineedit.setValidator(validator)
self.klass_lineedit.setText('UserClass')
self.klass_lineedit.textChanged.connect(lambda x: self.Changed.emit())
layout.addWidget(self.klass_lineedit, 0, 1, 1, 1)
self.name_label = QtWidgets.QLabel('Name', self)
layout.addWidget(self.name_label, 1, 0, 1, 1)
self.name_lineedit = QtWidgets.QLineEdit(self)
self.name_lineedit.setText('Name of function')
self.name_lineedit.textChanged.connect(lambda x: self.Changed.emit())
layout.addWidget(self.name_lineedit, 1, 1, 1, 1)
self.group_label = QtWidgets.QLabel('Group', self)
layout.addWidget(self.group_label, 2, 0, 1, 1)
self.group_lineedit = QtWidgets.QLineEdit(self)
self.group_lineedit.setText('User-defined')
self.group_lineedit.textChanged.connect(lambda x: self.Changed.emit())
layout.addWidget(self.group_lineedit, 2, 1, 1, 1)
self.eq_label = QtWidgets.QLabel('Disp. equation', self)
layout.addWidget(self.eq_label, 3, 0, 1, 1)
self.eq_lineedit = QtWidgets.QLineEdit(self)
self.eq_lineedit.textChanged.connect(lambda x: self.Changed.emit())
layout.addWidget(self.eq_lineedit, 3, 1, 1, 1)
self.setLayout(layout)
def get_strings(self) -> str:
if self.klass_lineedit.text() == '':
raise ValueError('Class name is empty')
stringi = f'class {self.klass_lineedit.text()}:\n' \
f' name = {self.name_lineedit.text()!r}\n' \
f' group = {self.group_lineedit.text()!r}\n' \
f' equation = {self.eq_lineedit.text()!r}\n'
return stringi
if __name__ == '__main__':
import sys
app = QtWidgets.QApplication([])
win = QUserFitCreator()
win.show()
sys.exit(app.exec())

284
src/gui_qt/fit/result.py Normal file
View File

@ -0,0 +1,284 @@
from math import isnan
from pyqtgraph import mkBrush
from nmreval.utils.text import convert
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, dict)
redoFit = QtCore.pyqtSignal(dict)
def __init__(self, results: list, management, parent=None):
super().__init__(parent=parent)
self.setupUi(self)
self._management = management
self._prevs = {}
self._models = {}
for (res, parts) in results:
idx = res.idx
data_k = management.data[idx]
if res.name not in self._models:
self._models[res.name] = []
self._models[res.name].append(idx)
self._prevs[idx] = []
for fit in data_k.get_fits():
self._prevs[idx].append((fit.name, fit.statistics, fit.nobs-fit.nvar))
self._results = {res.idx: res for (res, _) in results}
self._parts = {res.idx: parts for (res, parts) in results}
self._opts = [(False, False) for _ in range(len(self._results))]
self.residplot = self.graphicsView.addPlot(row=0, col=0)
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.residplot.addItem(self.resid_graph)
self.residplot.addItem(self.resid_graph_imag)
self.residplot.setLabel('left', 'Residual')
self.fitplot = self.graphicsView.addPlot(row=1, col=0)
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.fitplot.addItem(self.data_graph)
self.fitplot.addItem(self.data_graph_imag)
self.fitplot.setLabel('left', 'Function')
self.fit_graph = PlotItem(x=[], y=[])
self.fit_graph_imag = PlotItem(x=[], y=[])
self.fitplot.addItem(self.fit_graph)
self.fitplot.addItem(self.fit_graph_imag)
self.cmap = RdBuCMap(vmin=-1, vmax=1)
self.sets_comboBox.blockSignals(True)
for n in self._models.keys():
self.sets_comboBox.addItem(n)
self.sets_comboBox.blockSignals(False)
self.set_parameter(0)
self.buttonBox.accepted.connect(self.accept)
self.param_tableWidget.itemClicked.connect(self.show_results)
self.param_tableWidget.horizontalHeader().sectionClicked.connect(lambda i: self.show_results(None, idx=i))
self.graph_checkBox.stateChanged.connect(lambda x: self.graph_comboBox.setEnabled(x == QtCore.Qt.Unchecked))
self.logy_box.stateChanged.connect(lambda x: self.fitplot.setLogMode(y=bool(x)))
self.logx_box.stateChanged.connect(lambda x: self.fitplot.setLogMode(x=bool(x)))
self.residplot.setXLink(self.fitplot)
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):
model_name = self.sets_comboBox.itemText(idx)
sets = self._models[model_name]
self.param_tableWidget.setColumnCount(len(sets))
r = self._results[sets[0]]
self.param_tableWidget.setRowCount(len(r.parameter))
for i, pval in enumerate(r.parameter.values()):
name = pval.full_name
p_header = QtWidgets.QTableWidgetItem(convert(name, 'tex', 'html', brackets=False))
self.param_tableWidget.setVerticalHeaderItem(i, p_header)
for i, set_id in enumerate(sets):
data_i = self._management[set_id]
header_item = QtWidgets.QTableWidgetItem(data_i.name)
header_item.setData(QtCore.Qt.UserRole, set_id)
self.param_tableWidget.setHorizontalHeaderItem(i, header_item)
res = self._results[set_id]
for j, pvalue in enumerate(res.parameter.values()):
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, i, QtWidgets.QTableWidgetItem('-'))
else:
self.param_tableWidget.setItem(2*j+1, i, QtWidgets.QTableWidgetItem())
item = QtWidgets.QTableWidgetItem(item_text)
self.param_tableWidget.setItem(j, i, item)
self.param_tableWidget.resizeColumnsToContents()
self.param_tableWidget.selectColumn(0)
self.show_results(None, idx=0)
@QtCore.pyqtSlot(int, name='on_reject_fit_checkBox_stateChanged')
@QtCore.pyqtSlot(int, name='on_del_prev_checkBox_stateChanged')
def change_opts(self, _):
idx = self.param_tableWidget.currentIndex().column()
self._opts[idx] = (self.reject_fit_checkBox.checkState() == QtCore.Qt.Checked,
self.del_prev_checkBox.checkState() == QtCore.Qt.Checked)
def show_results(self, item, idx=None):
if item is not None:
idx = self.param_tableWidget.indexFromItem(item).column()
set_id = self.param_tableWidget.horizontalHeaderItem(idx).data(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)
def set_plot(self, idx: str):
res = self._results[idx]
iscomplex = res.iscomplex
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)
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=[])
self.fitplot.setLogMode(x=res.islog)
self.residplot.setLogMode(x=res.islog)
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._prevs[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._prevs[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._prevs[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._prevs[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()
if self.partial_checkBox.checkState() == QtCore.Qt.Checked:
self.closed.emit(self._results, self._opts, graph, plot_fits, self._parts)
else:
self.closed.emit(self._results, self._opts, graph, plot_fits, {})
self.accept()
else:
self.reject()