0
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2025-07-14 03:31:50 +02:00

feat: add Dap dialog widget

This commit is contained in:
2024-08-29 17:15:44 +02:00
parent 162e0ae78b
commit 9781b77de2
18 changed files with 826 additions and 216 deletions

View File

@ -26,6 +26,7 @@ class Widgets(str, enum.Enum):
DeviceBrowser = "DeviceBrowser"
DeviceComboBox = "DeviceComboBox"
DeviceLineEdit = "DeviceLineEdit"
LMFitDialog = "LMFitDialog"
PositionerBox = "PositionerBox"
PositionerControlLine = "PositionerControlLine"
ResetButton = "ResetButton"
@ -1825,15 +1826,6 @@ class BECWaveform(RPCBase):
x_entry(str): Entry of the x signal.
"""
@rpc_call
def get_dap_params(self) -> "dict":
"""
Get the DAP parameters of all DAP curves.
Returns:
dict: DAP parameters of all DAP curves.
"""
@rpc_call
def remove_curve(self, *identifiers):
"""
@ -2096,10 +2088,10 @@ class BECWaveformWidget(RPCBase):
self,
x_name: "str",
y_name: "str",
dap: "str",
x_entry: "str | None" = None,
y_entry: "str | None" = None,
color: "str | None" = None,
dap: "str" = "GaussianModel",
validate_bec: "bool" = True,
**kwargs,
) -> "BECCurve":
@ -2120,15 +2112,6 @@ class BECWaveformWidget(RPCBase):
BECCurve: The curve object.
"""
@rpc_call
def get_dap_params(self) -> "dict":
"""
Get the DAP parameters of all DAP curves.
Returns:
dict: DAP parameters of all DAP curves.
"""
@rpc_call
def remove_curve(self, *identifiers):
"""
@ -2313,7 +2296,7 @@ class BECWaveformWidget(RPCBase):
class DarkModeButton(RPCBase):
@rpc_call
def toggle_dark_mode(self) -> None:
def toggle_dark_mode(self) -> "None":
"""
Toggle the dark mode state. This will change the theme of the entire
application to dark or light mode.
@ -2392,6 +2375,24 @@ class DeviceLineEdit(RPCBase):
"""
class LMFitDialog(RPCBase):
@property
@rpc_call
def _config_dict(self) -> "dict":
"""
Get the configuration of the widget.
Returns:
dict: The configuration of the widget.
"""
@rpc_call
def _get_all_rpc(self) -> "dict":
"""
Get all registered RPC objects.
"""
class PositionerBox(RPCBase):
@rpc_call
def set_positioner(self, positioner: "str | Positioner"):

View File

@ -50,7 +50,6 @@ class BECWaveform(BECPlotBase):
"plot",
"add_dap",
"set_x",
"get_dap_params",
"remove_curve",
"scan_history",
"curves",
@ -74,8 +73,8 @@ class BECWaveform(BECPlotBase):
]
scan_signal_update = pyqtSignal()
async_signal_update = pyqtSignal()
dap_params_update = pyqtSignal(dict)
dap_summary_update = pyqtSignal(dict)
dap_params_update = pyqtSignal(dict, dict)
dap_summary_update = pyqtSignal(dict, dict)
autorange_signal = pyqtSignal()
new_scan = pyqtSignal()
@ -1085,8 +1084,9 @@ class BECWaveform(BECPlotBase):
curve.setData(x, y)
curve.dap_params = msg["data"][1]["fit_parameters"]
curve.dap_summary = msg["data"][1]["fit_summary"]
self.dap_params_update.emit(curve.dap_params)
self.dap_summary_update.emit(curve.dap_summary)
metadata.update({"curve_id": curve_id_request})
self.dap_params_update.emit(curve.dap_params, metadata)
self.dap_summary_update.emit(curve.dap_summary, metadata)
break
@Slot(dict, dict)

View File

@ -0,0 +1 @@
{'files': ['lmfit_dialog.py']}

View File

@ -0,0 +1,54 @@
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from qtpy.QtDesigner import QDesignerCustomWidgetInterface
from bec_widgets.utils.bec_designer import designer_material_icon
from bec_widgets.widgets.lmfit_dialog.lmfit_dialog import LMFitDialog
DOM_XML = """
<ui language='c++'>
<widget class='LMFitDialog' name='lm_fit_dialog'>
</widget>
</ui>
"""
class LMFitDialogPlugin(QDesignerCustomWidgetInterface): # pragma: no cover
def __init__(self):
super().__init__()
self._form_editor = None
def createWidget(self, parent):
t = LMFitDialog(parent)
return t
def domXml(self):
return DOM_XML
def group(self):
return ""
def icon(self):
return designer_material_icon(LMFitDialog.ICON_NAME)
def includeFile(self):
return "lm_fit_dialog"
def initialize(self, form_editor):
self._form_editor = form_editor
def isContainer(self):
return False
def isInitialized(self):
return self._form_editor is not None
def name(self):
return "LMFitDialog"
def toolTip(self):
return "LMFitDialog"
def whatsThis(self):
return self.toolTip()

View File

@ -0,0 +1,185 @@
import os
from bec_lib.endpoints import MessageEndpoints
from bec_lib.logger import bec_logger
from qtpy.QtCore import Property, Signal, Slot
from qtpy.QtWidgets import QTreeWidgetItem, QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_widget import BECWidget
logger = bec_logger.logger
class LMFitDialog(BECWidget, QWidget):
ICON_NAME = "bike_lane"
selected_fit = Signal(str)
def __init__(
self,
parent=None,
client=None,
config=None,
target_widget=None,
gui_id: str | None = None,
ui_file="lmfit_dialog_vertical.ui",
):
super().__init__(client=client, config=config, gui_id=gui_id)
QWidget.__init__(self, parent=parent)
self._ui_file = ui_file
self.target_widget = target_widget
current_path = os.path.dirname(__file__)
self.ui = UILoader(self).loader(os.path.join(current_path, self._ui_file))
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.ui)
self.summary_data = {}
self._fit_curve_id = None
self._deci_precision = 3
self.ui.curve_list.currentItemChanged.connect(self.display_fit_details)
self.setLayout(self.layout)
@Property(bool)
def hide_curve_selection(self):
"""Property for showing the curve selection."""
return not self.ui.group_curve_selection.isVisible()
@hide_curve_selection.setter
def hide_curve_selection(self, show: bool):
"""Setter for showing the curve selection.
Args:
show (bool): Whether to show the curve selection.
"""
self.ui.group_curve_selection.setVisible(not show)
@property
def fit_curve_id(self):
"""Property for the currently displayed fit curve_id."""
return self._fit_curve_id
@fit_curve_id.setter
def fit_curve_id(self, curve_id: str):
"""Setter for the currently displayed fit curve_id.
Args:
fit_curve_id (str): The curve_id of the fit curve to be displayed.
"""
self._fit_curve_id = curve_id
self.selected_fit.emit(curve_id)
@Slot(str)
def remove_dap_data(self, curve_id: str):
"""Remove the DAP data for the given curve_id.
Args:
curve_id (str): The curve_id of the DAP data to be removed.
"""
self.summary_data.pop(curve_id, None)
self.refresh_curve_list()
@Slot(str)
def select_curve(self, curve_id: str):
"""Select active curve_id in the curve list.
Args:
curve_id (str): curve_id to be selected.
"""
self.fit_curve_id = curve_id
@Slot(dict, dict)
def update_summary_tree(self, data: dict, metadata: dict):
"""Update the summary tree with the given data.
Args:
data (dict): Data for the DAP Summary.
metadata (dict): Metadata of the fit curve.
"""
curve_id = metadata.get("curve_id", "")
self.summary_data.update({curve_id: data})
self.refresh_curve_list()
if self.fit_curve_id is None:
self.fit_curve_id = curve_id
for index in range(self.ui.curve_list.count()):
item = self.ui.curve_list.item(index)
if item.text() == curve_id:
self.ui.curve_list.setCurrentItem(item)
if curve_id != self.fit_curve_id:
return
if data is None:
return
self.ui.summary_tree.clear()
properties = [
("Model", data.get("model", "")),
("Method", data.get("method", "")),
("Chi-Squared", f"{data.get('chisqr', 0.0):.{self._deci_precision}f}"),
("Reduced Chi-Squared", f"{data.get('redchi', 0.0):.{self._deci_precision}f}"),
("R-Squared", f"{data.get('rsquared', 0.0):.{self._deci_precision}f}"),
("Message", data.get("message", "")),
]
for prop, val in properties:
QTreeWidgetItem(self.ui.summary_tree, [prop, val])
self.update_param_tree(data.get("params", []))
def _update_summary_data(self, curve_id: str, data: dict):
"""Update the summary data with the given data.
Args:
curve_id (str): The curve_id of the fit curve.
data (dict): The data to be updated.
"""
self.summary_data.update({curve_id: data})
if self.fit_curve_id is not None:
return
self.fit_curve_id = curve_id
def update_param_tree(self, params):
"""Update the parameter tree with the given parameters.
Args:
params (list): List of LMFit parameters for the fit curve.
"""
self.ui.param_tree.clear()
for param in params:
param_name, param_value, param_std = (
param[0],
f"{param[1]:.{self._deci_precision}f}",
f"{param[7]:.{self._deci_precision}f}",
)
QTreeWidgetItem(self.ui.param_tree, [param_name, param_value, param_std])
def populate_curve_list(self):
"""Populate the curve list with the available fit curves."""
for curve_name in self.summary_data.keys():
self.ui.curve_list.addItem(curve_name)
def refresh_curve_list(self):
"""Refresh the curve list with the updated data."""
self.ui.curve_list.clear()
self.populate_curve_list()
def display_fit_details(self, current):
"""Callback for displaying the fit details of the selected curve.
Args:
current: The current item in the curve list.
"""
if current:
curve_name = current.text()
self.fit_curve_id = curve_name
data = self.summary_data[curve_name]
if data is None:
return
self.update_summary_tree(data, {"curve_id": curve_name})
if __name__ == "__main__":
import sys
from qtpy.QtWidgets import QApplication
app = QApplication(sys.argv)
dialog = LMFitDialog()
dialog.show()
sys.exit(app.exec_())

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>655</width>
<height>520</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QSplitter" name="splitter_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::Shape::VLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>1</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="opaqueResize">
<bool>true</bool>
</property>
<property name="childrenCollapsible">
<bool>true</bool>
</property>
<widget class="QGroupBox" name="group_curve_selection">
<property name="title">
<string>Select Curve</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QListWidget" name="curve_list"/>
</item>
</layout>
</widget>
<widget class="QSplitter" name="splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<widget class="QGroupBox" name="group_summary">
<property name="title">
<string>Fit Summary</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTreeWidget" name="summary_tree">
<property name="uniformRowHeights">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>Property</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QGroupBox" name="group_parameters">
<property name="title">
<string>Parameter Details</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTreeWidget" name="param_tree">
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
<column>
<property name="text">
<string>Std</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>274</width>
<height>568</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_4" stretch="0,0,0">
<item>
<widget class="QGroupBox" name="group_curve_selection">
<property name="title">
<string>Select Curve</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout" stretch="3">
<item>
<widget class="QListWidget" name="curve_list"/>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="group_summary">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>250</width>
<height>200</height>
</size>
</property>
<property name="title">
<string>Fit Summary</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTreeWidget" name="summary_tree">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<property name="uniformRowHeights">
<bool>false</bool>
</property>
<attribute name="headerDefaultSectionSize">
<number>80</number>
</attribute>
<column>
<property name="text">
<string>Property</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QGroupBox" name="group_parameters">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>250</width>
<height>200</height>
</size>
</property>
<property name="title">
<string>Parameter Details</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTreeWidget" name="param_tree">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
<attribute name="headerDefaultSectionSize">
<number>80</number>
</attribute>
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
<column>
<property name="text">
<string>Std</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,15 @@
def main(): # pragma: no cover
from qtpy import PYSIDE6
if not PYSIDE6:
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
return
from PySide6.QtDesigner import QPyDesignerCustomWidgetCollection
from bec_widgets.widgets.lmfit_dialog.lm_fit_dialog_plugin import LMFitDialogPlugin
QPyDesignerCustomWidgetCollection.addCustomWidget(LMFitDialogPlugin())
if __name__ == "__main__": # pragma: no cover
main()

View File

@ -1,127 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>800</width>
<height>600</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QSplitter" name="splitter_2">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>1</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="frameShape">
<enum>QFrame::Shape::VLine</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Shadow::Plain</enum>
</property>
<property name="lineWidth">
<number>1</number>
</property>
<property name="orientation">
<enum>Qt::Orientation::Horizontal</enum>
</property>
<property name="opaqueResize">
<bool>true</bool>
</property>
<property name="childrenCollapsible">
<bool>true</bool>
</property>
<widget class="QGroupBox" name="group_curve_selection">
<property name="title">
<string>Select Curve</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPushButton" name="refresh_button">
<property name="text">
<string>Refresh DAP Summary</string>
</property>
</widget>
</item>
<item>
<widget class="QListWidget" name="curve_list"/>
</item>
</layout>
</widget>
<widget class="QSplitter" name="splitter">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>2</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Orientation::Vertical</enum>
</property>
<widget class="QGroupBox" name="group_summary">
<property name="title">
<string>Fit Summary</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QTreeWidget" name="summary_tree">
<property name="uniformRowHeights">
<bool>false</bool>
</property>
<column>
<property name="text">
<string>Property</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
<widget class="QGroupBox" name="group_parameters">
<property name="title">
<string>Parameter Details</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QTreeWidget" name="param_tree">
<column>
<property name="text">
<string>Parameter</string>
</property>
</column>
<column>
<property name="text">
<string>Value</string>
</property>
</column>
<column>
<property name="text">
<string>Std</string>
</property>
</column>
</widget>
</item>
</layout>
</widget>
</widget>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -4,66 +4,26 @@ from qtpy.QtWidgets import QDialog, QTreeWidgetItem, QVBoxLayout
from bec_widgets.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import UILoader
from bec_widgets.widgets.lmfit_dialog.lmfit_dialog import LMFitDialog
class FitSummaryWidget(QDialog):
def __init__(self, parent=None, target_widget=None):
super().__init__(parent=parent)
self.target_widget = target_widget
self.summary_data = self.target_widget.get_dap_summary()
self.setModal(True)
current_path = os.path.dirname(__file__)
self.ui = UILoader(self).loader(os.path.join(current_path, "dap_summary.ui"))
self.target_widget = target_widget
self.dap_dialog = LMFitDialog(parent=self, ui_file="lmfit_dialog_compact.ui")
self.layout = QVBoxLayout(self)
self.layout.addWidget(self.ui)
self.layout.addWidget(self.dap_dialog)
self.target_widget.dap_summary_update.connect(self.dap_dialog.update_summary_tree)
self.setLayout(self.layout)
self._get_dap_from_target_widget()
self.ui.curve_list.currentItemChanged.connect(self.display_fit_details)
self.ui.refresh_button.clicked.connect(self.refresh_dap)
self.populate_curve_list()
def populate_curve_list(self):
for curve_name in self.summary_data.keys():
self.ui.curve_list.addItem(curve_name)
def display_fit_details(self, current):
if current:
curve_name = current.text()
data = self.summary_data[curve_name]
if data is None:
return
self.refresh_trees(data)
@Slot()
def refresh_dap(self):
self.ui.curve_list.clear()
self.summary_data = self.target_widget.get_dap_summary()
self.populate_curve_list()
def refresh_trees(self, data):
self.update_summary_tree(data)
self.update_param_tree(data["params"])
def update_summary_tree(self, data):
self.ui.summary_tree.clear()
properties = [
("Model", data.get("model", "")),
("Method", data.get("method", "")),
("Chi-Squared", str(data.get("chisqr", ""))),
("Reduced Chi-Squared", str(data.get("redchi", ""))),
("AIC", str(data.get("aic", ""))),
("BIC", str(data.get("bic", ""))),
("R-Squared", str(data.get("rsquared", ""))),
("Message", data.get("message", "")),
]
for prop, val in properties:
QTreeWidgetItem(self.ui.summary_tree, [prop, val])
def update_param_tree(self, params):
self.ui.param_tree.clear()
for param in params:
param_name, param_value, param_std = param[0], str(param[1]), str(param[7])
QTreeWidgetItem(self.ui.param_tree, [param_name, param_value, param_std])
def _get_dap_from_target_widget(self) -> None:
"""Get the DAP data from the target widget and update the DAP dialog manually on creation."""
dap_summary = self.target_widget.get_dap_summary()
for curve_id, data in dap_summary.items():
md = {"curve_id": curve_id}
self.dap_dialog.update_summary_tree(data=data, metadata=md)

View File

@ -33,7 +33,6 @@ class BECWaveformWidget(BECWidget, QWidget):
"curves",
"plot",
"add_dap",
"get_dap_params",
"remove_curve",
"scan_history",
"get_all_data",
@ -55,8 +54,8 @@ class BECWaveformWidget(BECWidget, QWidget):
]
scan_signal_update = Signal()
async_signal_update = Signal()
dap_params_update = Signal(dict)
dap_summary_update = Signal(dict)
dap_summary_update = Signal(dict, dict)
dap_params_update = Signal(dict, dict)
autorange_signal = Signal()
new_scan = Signal()
crosshair_position_changed = Signal(tuple)
@ -218,7 +217,7 @@ class BECWaveformWidget(BECWidget, QWidget):
def show_fit_summary_dialog(self):
dialog = FitSummaryWidget(target_widget=self)
dialog.resize(800, 600)
dialog.show()
dialog.exec()
###################################
# User Access Methods from Waveform
@ -257,7 +256,7 @@ class BECWaveformWidget(BECWidget, QWidget):
"""
self.waveform.set_colormap(colormap)
@SafeSlot(popup_error=True)
@SafeSlot(str, popup_error=True)
def set_x(self, x_name: str, x_entry: str | None = None):
"""
Change the x axis of the plot widget.
@ -272,7 +271,7 @@ class BECWaveformWidget(BECWidget, QWidget):
"""
self.waveform.set_x(x_name, x_entry)
@SafeSlot(popup_error=True)
@SafeSlot(str, popup_error=True)
def plot(
self,
arg1: list | np.ndarray | str | None = None,
@ -331,15 +330,16 @@ class BECWaveformWidget(BECWidget, QWidget):
**kwargs,
)
@SafeSlot(popup_error=True)
@SafeSlot(str, str, str, popup_error=True)
def add_dap(
self,
x_name: str,
y_name: str,
dap: str,
x_entry: str | None = None,
y_entry: str | None = None,
color: str | None = None,
dap: str = "GaussianModel",
# dap: str = "GaussianModel",
validate_bec: bool = True,
**kwargs,
) -> BECCurve:

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@ -0,0 +1,50 @@
(user.widgets.lmfit_dialog)=
# LMFit Dialog
````{tab} Overview
The [`LMFit Dialog`](/api_reference/_autosummary/bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog) is a widget that is developed to be used togther with the [`BECWaveformWidget`](/api_reference/_autosummary/bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget). The `BECWaveformWidget` allows user to submit a fit request to BEC's [DAP server](https://bec.readthedocs.io/en/latest/developer/getting_started/architecture.html) choosing from a selection of [LMFit models](https://lmfit.github.io/lmfit-py/builtin_models.html#) to fit monitored data sources. The `LMFit Dialog` provides an interface to monitor these fits, including statistics and fit parameters in real time.
Within the `BECWaveformWidget`, the dialog is accessible via the toolbar and will be automatically linked to the current waveform widget. For a more customised use, we can embed the `LMFit Dialog` in a larger GUI using the *BEC Designer*. In this case, one has to connect the [`update_summary_tree`](/api_reference/_autosummary/bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog.rst#bec_widgets.widgets.lmfit_dialog.lmfit_dialog.LMFitDialog.update_summary_tree) slot of the LMFit Dialog to the [`dap_summary_update`](/api_reference/_autosummary/bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget.rst#bec_widgets.widgets.waveform.waveform_widget.BECWaveformWidget.dap_summary_update) signal of the BECWaveformWidget to ensure its functionality.
## Key Features:
- **Fit Summary**: Display updates on LMFit DAP processes and fit statistics.
- **Fit Parameter**: Display current fit parameter.
- **BECWaveformWidget Integration**: Directly connect to BECWaveformWidget to display fit statistics and parameters.
```{figure} /assets/widget_screenshots/lmfit_dialog.png
---
name: lmfit_dialog
---
LMFit Dialog
```
````
````{tab} Connect in BEC Designer
The `LMFit Dialog` widget can be connected to a `BECWaveformWidget` to display fit statistics and parameters from the LMFit DAP process hooked up to the waveform widget. You can use the signal/slot editor from the BEC Designer to connect the `dap_summary_update` signal of the BECWaveformWidget to the `update_summary_tree` slot of the LMFit Dialog.
```{figure} /assets/widget_screenshots/lmfit_dialog_connect.png
````
````{tab} Connect in Python
It is also possible to directly connect the `dap_summary_update` signal of the BECWaveformWidget to the `update_summary_tree` slot of the LMFit Dialog in Python.
```python
waveform = BECWaveformWidget(...)
lmfit_dialog = LMFitDialog(...)
waveform.dap_summary_update.connect(lmfit_dialog.update_summary_tree)
```
````
````{tab} API
```{eval-rst}
.. include:: /api_reference/_autosummary/bec_widgets.cli.client.LMFitDialog.rst
```
````

View File

@ -190,6 +190,14 @@ Display spinner widget for loading or device movement.
Display position of motor withing its limits.
```
```{grid-item-card} LMFit Dialog
:link: user.widgets.lmfit_dialog
:link-type: ref
:img-top: /assets/widget_screenshots/lmfit_dialog.png
Display DAP summaries of LMFit models in a window.
```
````
```{toctree}
@ -216,5 +224,6 @@ toggle/toggle.md
spinner/spinner.md
device_input/device_input.md
position_indicator/position_indicator.md
lmfit_dialog/lmfit_dialog.md
```

View File

@ -0,0 +1,183 @@
from unittest import mock
import numpy as np
import pytest
from bec_widgets.widgets.lmfit_dialog.lmfit_dialog import LMFitDialog
from .client_mocks import mocked_client
from .conftest import create_widget
@pytest.fixture(scope="function")
def lmfit_dialog(qtbot, mocked_client):
"""Fixture for LMFitDialog widget"""
db = create_widget(qtbot, LMFitDialog, client=mocked_client)
yield db
@pytest.fixture(scope="function")
def lmfit_message():
"""Fixture for lmfit summary message"""
yield {
"model": "Model(breit_wigner)",
"method": "leastsq",
"ndata": 4,
"nvarys": 4,
"nfree": 0,
"chisqr": 1.2583132407517716,
"redchi": 1.2583132407517716,
"aic": 3.3739110606840716,
"bic": 0.9190885051636339,
"rsquared": 0.9650468544235619,
"nfev": 2498,
"max_nfev": 10000,
"aborted": False,
"errorbars": True,
"success": True,
"message": "Fit succeeded.",
"lmdif_message": "Both actual and predicted relative reductions in the sum of squares\n are at most 0.000000",
"ier": 1,
"nan_policy": "raise",
"scale_covar": True,
"calc_covar": True,
"ci_out": None,
"col_deriv": False,
"flatchain": None,
"call_kws": {
"Dfun": None,
"full_output": 1,
"col_deriv": 0,
"ftol": 1.5e-08,
"xtol": 1.5e-08,
"gtol": 0.0,
"maxfev": 20000,
"epsfcn": 1e-10,
"factor": 100,
"diag": None,
},
"var_names": ["amplitude", "center", "sigma", "q"],
"user_options": None,
"kws": {},
"init_values": {"amplitude": 1.0, "center": 0.0, "sigma": 1.0, "q": 1.0},
"best_values": {
"amplitude": 1.5824142042890903,
"center": -2.8415356591834326,
"sigma": 0.0002550847234503717,
"q": -259.8514775889427,
},
"params": [
[
"amplitude",
1.5824142042890903,
True,
None,
-np.inf,
np.inf,
None,
1.3249185295752495,
{
"center": 0.8429146627203449,
"sigma": -0.8362891947010586,
"q": -0.8362890089256452,
},
1.0,
None,
],
[
"center",
-2.8415356591834326,
True,
None,
-np.inf,
np.inf,
None,
0.5077201488266584,
{
"amplitude": 0.8429146627203449,
"sigma": -0.9987662050702767,
"q": -0.9987662962832818,
},
0.0,
None,
],
[
"sigma",
0.0002550847234503717,
True,
None,
0.0,
np.inf,
None,
113.2533064536711,
{
"amplitude": -0.8362891947010586,
"center": -0.9987662050702767,
"q": 0.999999999997876,
},
1.0,
None,
],
[
"q",
-259.8514775889427,
True,
None,
-np.inf,
np.inf,
None,
114893884.64553572,
{
"amplitude": -0.8362890089256452,
"center": -0.9987662962832818,
"sigma": 0.999999999997876,
},
1.0,
None,
],
],
}
def test_fit_curve_id(lmfit_dialog):
"""Test hide_curve_selection property"""
my_callback = mock.MagicMock()
lmfit_dialog.selected_fit.connect(my_callback)
assert lmfit_dialog.fit_curve_id is None
lmfit_dialog.fit_curve_id = "test_curve_id"
assert lmfit_dialog.fit_curve_id == "test_curve_id"
assert my_callback.call_count == 1
assert my_callback.call_args == mock.call("test_curve_id")
def test_remove_dap_data(lmfit_dialog):
"""Test remove_dap_data method"""
lmfit_dialog.summary_data = {"test": "data", "test2": "data2"}
lmfit_dialog.refresh_curve_list()
# Only 2 items
assert lmfit_dialog.ui.curve_list.count() == 2
lmfit_dialog.remove_dap_data("test")
assert lmfit_dialog.summary_data == {"test2": "data2"}
assert lmfit_dialog.ui.curve_list.count() == 1
# Test removing non-existing data
# Nothing should happen
lmfit_dialog.remove_dap_data("test_not_there")
assert lmfit_dialog.summary_data == {"test2": "data2"}
assert lmfit_dialog.ui.curve_list.count() == 1
def test_update_summary_tree(lmfit_dialog, lmfit_message):
"""Test display_fit_details method"""
lmfit_dialog.update_summary_tree(data=lmfit_message, metadata={"curve_id": "test_curve_id"})
# Check if the data is updated
assert lmfit_dialog.summary_data == {"test_curve_id": lmfit_message}
# Check if the curve list is updated
assert lmfit_dialog.ui.curve_list.count() == 1
# Check summary tree is updated
assert lmfit_dialog.ui.summary_tree.topLevelItemCount() == 6
assert lmfit_dialog.ui.summary_tree.topLevelItem(0).text(0) == "Model"
assert lmfit_dialog.ui.summary_tree.topLevelItem(0).text(1) == "Model(breit_wigner)"
# Check fit params tree is updated
assert lmfit_dialog.ui.param_tree.topLevelItemCount() == 4
assert lmfit_dialog.ui.param_tree.topLevelItem(0).text(0) == "amplitude"
assert lmfit_dialog.ui.param_tree.topLevelItem(0).text(1) == "1.582"

View File

@ -9,9 +9,13 @@ from bec_widgets.qt_utils.settings_dialog import SettingsDialog
from bec_widgets.utils.colors import apply_theme, get_theme_palette, set_theme
from bec_widgets.widgets.figure.plots.axis_settings import AxisSettings
from bec_widgets.widgets.waveform.waveform_popups.curve_dialog.curve_dialog import CurveSettings
from bec_widgets.widgets.waveform.waveform_popups.dap_summary_dialog.dap_summary_dialog import (
FitSummaryWidget,
)
from bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget
from .client_mocks import mocked_client
from .conftest import create_widget
@pytest.fixture
@ -97,7 +101,7 @@ def test_waveform_plot_scan_curves(waveform_widget, mock_waveform):
def test_waveform_widget_add_dap(waveform_widget, mock_waveform):
waveform_widget.add_dap(x_name="samx", y_name="bpm4i")
waveform_widget.add_dap(x_name="samx", y_name="bpm4i", dap="GaussianModel")
waveform_widget.waveform.add_dap.assert_called_once_with(
x_name="samx",
y_name="bpm4i",
@ -252,7 +256,7 @@ def test_toolbar_fit_params_action_triggered(qtbot, waveform_widget):
) as MockFitSummaryWidget:
mock_dialog_instance = MockFitSummaryWidget.return_value
action.trigger()
mock_dialog_instance.show.assert_called_once()
mock_dialog_instance.exec.assert_called_once()
def test_enable_mouse_pan_mode(qtbot, waveform_widget):
@ -378,6 +382,14 @@ def test_curve_dialog_dap(qtbot, waveform_widget):
assert len(waveform_widget.curves) == 2
def test_fit_dialog_summary(qtbot, waveform_widget):
"""Test the fit dialog summary widget"""
waveform_widget.plot(x_name="samx", y_name="bpm4i", dap="GaussianModel")
fit_dialog_summary = create_widget(qtbot, FitSummaryWidget, target_widget=waveform_widget)
assert fit_dialog_summary.dap_dialog.fit_curve_id == "bpm4i-bpm4i-GaussianModel"
assert fit_dialog_summary.dap_dialog.ui.curve_list.count() == 1
###################################
# Axis Dialog Tests
###################################