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" DeviceBrowser = "DeviceBrowser"
DeviceComboBox = "DeviceComboBox" DeviceComboBox = "DeviceComboBox"
DeviceLineEdit = "DeviceLineEdit" DeviceLineEdit = "DeviceLineEdit"
LMFitDialog = "LMFitDialog"
PositionerBox = "PositionerBox" PositionerBox = "PositionerBox"
PositionerControlLine = "PositionerControlLine" PositionerControlLine = "PositionerControlLine"
ResetButton = "ResetButton" ResetButton = "ResetButton"
@ -1825,15 +1826,6 @@ class BECWaveform(RPCBase):
x_entry(str): Entry of the x signal. 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 @rpc_call
def remove_curve(self, *identifiers): def remove_curve(self, *identifiers):
""" """
@ -2096,10 +2088,10 @@ class BECWaveformWidget(RPCBase):
self, self,
x_name: "str", x_name: "str",
y_name: "str", y_name: "str",
dap: "str",
x_entry: "str | None" = None, x_entry: "str | None" = None,
y_entry: "str | None" = None, y_entry: "str | None" = None,
color: "str | None" = None, color: "str | None" = None,
dap: "str" = "GaussianModel",
validate_bec: "bool" = True, validate_bec: "bool" = True,
**kwargs, **kwargs,
) -> "BECCurve": ) -> "BECCurve":
@ -2120,15 +2112,6 @@ class BECWaveformWidget(RPCBase):
BECCurve: The curve object. 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 @rpc_call
def remove_curve(self, *identifiers): def remove_curve(self, *identifiers):
""" """
@ -2313,7 +2296,7 @@ class BECWaveformWidget(RPCBase):
class DarkModeButton(RPCBase): class DarkModeButton(RPCBase):
@rpc_call @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 Toggle the dark mode state. This will change the theme of the entire
application to dark or light mode. 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): class PositionerBox(RPCBase):
@rpc_call @rpc_call
def set_positioner(self, positioner: "str | Positioner"): def set_positioner(self, positioner: "str | Positioner"):

View File

@ -50,7 +50,6 @@ class BECWaveform(BECPlotBase):
"plot", "plot",
"add_dap", "add_dap",
"set_x", "set_x",
"get_dap_params",
"remove_curve", "remove_curve",
"scan_history", "scan_history",
"curves", "curves",
@ -74,8 +73,8 @@ class BECWaveform(BECPlotBase):
] ]
scan_signal_update = pyqtSignal() scan_signal_update = pyqtSignal()
async_signal_update = pyqtSignal() async_signal_update = pyqtSignal()
dap_params_update = pyqtSignal(dict) dap_params_update = pyqtSignal(dict, dict)
dap_summary_update = pyqtSignal(dict) dap_summary_update = pyqtSignal(dict, dict)
autorange_signal = pyqtSignal() autorange_signal = pyqtSignal()
new_scan = pyqtSignal() new_scan = pyqtSignal()
@ -1085,8 +1084,9 @@ class BECWaveform(BECPlotBase):
curve.setData(x, y) curve.setData(x, y)
curve.dap_params = msg["data"][1]["fit_parameters"] curve.dap_params = msg["data"][1]["fit_parameters"]
curve.dap_summary = msg["data"][1]["fit_summary"] curve.dap_summary = msg["data"][1]["fit_summary"]
self.dap_params_update.emit(curve.dap_params) metadata.update({"curve_id": curve_id_request})
self.dap_summary_update.emit(curve.dap_summary) self.dap_params_update.emit(curve.dap_params, metadata)
self.dap_summary_update.emit(curve.dap_summary, metadata)
break break
@Slot(dict, dict) @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.qt_utils.error_popups import SafeSlot as Slot
from bec_widgets.utils import UILoader from bec_widgets.utils import UILoader
from bec_widgets.widgets.lmfit_dialog.lmfit_dialog import LMFitDialog
class FitSummaryWidget(QDialog): class FitSummaryWidget(QDialog):
def __init__(self, parent=None, target_widget=None): def __init__(self, parent=None, target_widget=None):
super().__init__(parent=parent) super().__init__(parent=parent)
self.target_widget = target_widget
self.summary_data = self.target_widget.get_dap_summary()
self.setModal(True) self.setModal(True)
self.target_widget = target_widget
current_path = os.path.dirname(__file__) self.dap_dialog = LMFitDialog(parent=self, ui_file="lmfit_dialog_compact.ui")
self.ui = UILoader(self).loader(os.path.join(current_path, "dap_summary.ui"))
self.layout = QVBoxLayout(self) 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) def _get_dap_from_target_widget(self) -> None:
self.ui.refresh_button.clicked.connect(self.refresh_dap) """Get the DAP data from the target widget and update the DAP dialog manually on creation."""
dap_summary = self.target_widget.get_dap_summary()
self.populate_curve_list() for curve_id, data in dap_summary.items():
md = {"curve_id": curve_id}
def populate_curve_list(self): self.dap_dialog.update_summary_tree(data=data, metadata=md)
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])

View File

@ -33,7 +33,6 @@ class BECWaveformWidget(BECWidget, QWidget):
"curves", "curves",
"plot", "plot",
"add_dap", "add_dap",
"get_dap_params",
"remove_curve", "remove_curve",
"scan_history", "scan_history",
"get_all_data", "get_all_data",
@ -55,8 +54,8 @@ class BECWaveformWidget(BECWidget, QWidget):
] ]
scan_signal_update = Signal() scan_signal_update = Signal()
async_signal_update = Signal() async_signal_update = Signal()
dap_params_update = Signal(dict) dap_summary_update = Signal(dict, dict)
dap_summary_update = Signal(dict) dap_params_update = Signal(dict, dict)
autorange_signal = Signal() autorange_signal = Signal()
new_scan = Signal() new_scan = Signal()
crosshair_position_changed = Signal(tuple) crosshair_position_changed = Signal(tuple)
@ -218,7 +217,7 @@ class BECWaveformWidget(BECWidget, QWidget):
def show_fit_summary_dialog(self): def show_fit_summary_dialog(self):
dialog = FitSummaryWidget(target_widget=self) dialog = FitSummaryWidget(target_widget=self)
dialog.resize(800, 600) dialog.resize(800, 600)
dialog.show() dialog.exec()
################################### ###################################
# User Access Methods from Waveform # User Access Methods from Waveform
@ -257,7 +256,7 @@ class BECWaveformWidget(BECWidget, QWidget):
""" """
self.waveform.set_colormap(colormap) 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): def set_x(self, x_name: str, x_entry: str | None = None):
""" """
Change the x axis of the plot widget. Change the x axis of the plot widget.
@ -272,7 +271,7 @@ class BECWaveformWidget(BECWidget, QWidget):
""" """
self.waveform.set_x(x_name, x_entry) self.waveform.set_x(x_name, x_entry)
@SafeSlot(popup_error=True) @SafeSlot(str, popup_error=True)
def plot( def plot(
self, self,
arg1: list | np.ndarray | str | None = None, arg1: list | np.ndarray | str | None = None,
@ -331,15 +330,16 @@ class BECWaveformWidget(BECWidget, QWidget):
**kwargs, **kwargs,
) )
@SafeSlot(popup_error=True) @SafeSlot(str, str, str, popup_error=True)
def add_dap( def add_dap(
self, self,
x_name: str, x_name: str,
y_name: str, y_name: str,
dap: str,
x_entry: str | None = None, x_entry: str | None = None,
y_entry: str | None = None, y_entry: str | None = None,
color: str | None = None, color: str | None = None,
dap: str = "GaussianModel", # dap: str = "GaussianModel",
validate_bec: bool = True, validate_bec: bool = True,
**kwargs, **kwargs,
) -> BECCurve: ) -> 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. 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} ```{toctree}
@ -216,5 +224,6 @@ toggle/toggle.md
spinner/spinner.md spinner/spinner.md
device_input/device_input.md device_input/device_input.md
position_indicator/position_indicator.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.utils.colors import apply_theme, get_theme_palette, set_theme
from bec_widgets.widgets.figure.plots.axis_settings import AxisSettings 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.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 bec_widgets.widgets.waveform.waveform_widget import BECWaveformWidget
from .client_mocks import mocked_client from .client_mocks import mocked_client
from .conftest import create_widget
@pytest.fixture @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): 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( waveform_widget.waveform.add_dap.assert_called_once_with(
x_name="samx", x_name="samx",
y_name="bpm4i", y_name="bpm4i",
@ -252,7 +256,7 @@ def test_toolbar_fit_params_action_triggered(qtbot, waveform_widget):
) as MockFitSummaryWidget: ) as MockFitSummaryWidget:
mock_dialog_instance = MockFitSummaryWidget.return_value mock_dialog_instance = MockFitSummaryWidget.return_value
action.trigger() 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): 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 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 # Axis Dialog Tests
################################### ###################################