nmreval/src/gui_qt/fit/fitfunction.py

263 lines
9.1 KiB
Python

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.lib.logger import logger
from nmreval.utils.text import convert
from ..lib.iconloading 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.user_functions = {}
self.functions = find_models(models)
self.read_and_load_functions()
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 read_and_load_functions(self):
"""
Reads user-defined fit functions from usermodels.py and adds them to list of fit function.
Previous class definitions are replaced.
"""
user_defined = []
try:
user_defined = find_models(config_paths() / 'usermodels.py')
except Exception as e:
import traceback, sys
exc_type, exc_value, exc_traceback = sys.exc_info()
_ = QtWidgets.QMessageBox.warning(
self,
'No user functions',
f'Loading user-defined function failed with exception:\n'
f'{"".join(traceback.format_exception(exc_type, exc_value, exc_traceback, limit=1))}')
logger.exception("Invalid usermodels.py", exc_info=e)
for model in user_defined:
name = model.__name__
if name not in self.user_functions:
self.functions.append(model)
self.user_functions[name] = model
else:
idx = self.functions.index(self.user_functions[name])
self.functions[idx] = model
self.user_functions[name] = model
self._types = []
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.blockSignals(True)
self.typecomboBox.clear()
self.typecomboBox.addItems(sorted(self._types))
self.typecomboBox.blockSignals(False)
self.change_group(0)
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, param_names=self.functions[idx].params)
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):
iscomplex = False
iterator = QtWidgets.QTreeWidgetItemIterator(self.functree)
while iterator.value():
item = iterator.value()
if item.checkState(0) != QtCore.Qt.CheckState.Unchecked:
f = self.functions[item.data(0, QtCore.Qt.UserRole)]
if hasattr(f, 'iscomplex') and f.iscomplex:
iscomplex = True
break
iterator += 1
return self.complex_comboBox.currentIndex() if 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()