diff --git a/bec_widgets/cli/client.py b/bec_widgets/cli/client.py
index 1a60668d..ae17455a 100644
--- a/bec_widgets/cli/client.py
+++ b/bec_widgets/cli/client.py
@@ -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"):
diff --git a/bec_widgets/widgets/figure/plots/waveform/waveform.py b/bec_widgets/widgets/figure/plots/waveform/waveform.py
index 6825ad74..20589f49 100644
--- a/bec_widgets/widgets/figure/plots/waveform/waveform.py
+++ b/bec_widgets/widgets/figure/plots/waveform/waveform.py
@@ -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)
diff --git a/bec_widgets/widgets/lmfit_dialog/__init__.py b/bec_widgets/widgets/lmfit_dialog/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/bec_widgets/widgets/lmfit_dialog/lm_fit_dialog.pyproject b/bec_widgets/widgets/lmfit_dialog/lm_fit_dialog.pyproject
new file mode 100644
index 00000000..181f4f8d
--- /dev/null
+++ b/bec_widgets/widgets/lmfit_dialog/lm_fit_dialog.pyproject
@@ -0,0 +1 @@
+{'files': ['lmfit_dialog.py']}
\ No newline at end of file
diff --git a/bec_widgets/widgets/lmfit_dialog/lm_fit_dialog_plugin.py b/bec_widgets/widgets/lmfit_dialog/lm_fit_dialog_plugin.py
new file mode 100644
index 00000000..90b712b5
--- /dev/null
+++ b/bec_widgets/widgets/lmfit_dialog/lm_fit_dialog_plugin.py
@@ -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 = """
+
+
+
+
+"""
+
+
+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()
diff --git a/bec_widgets/widgets/lmfit_dialog/lmfit_dialog.py b/bec_widgets/widgets/lmfit_dialog/lmfit_dialog.py
new file mode 100644
index 00000000..c30aee17
--- /dev/null
+++ b/bec_widgets/widgets/lmfit_dialog/lmfit_dialog.py
@@ -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_())
diff --git a/bec_widgets/widgets/lmfit_dialog/lmfit_dialog_compact.ui b/bec_widgets/widgets/lmfit_dialog/lmfit_dialog_compact.ui
new file mode 100644
index 00000000..086d0cf6
--- /dev/null
+++ b/bec_widgets/widgets/lmfit_dialog/lmfit_dialog_compact.ui
@@ -0,0 +1,120 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 655
+ 520
+
+
+
+ Form
+
+
+ -
+
+
+
+ 1
+ 0
+
+
+
+ QFrame::Shape::VLine
+
+
+ QFrame::Shadow::Plain
+
+
+ 1
+
+
+ Qt::Orientation::Horizontal
+
+
+ true
+
+
+ true
+
+
+
+ Select Curve
+
+
+
-
+
+
+
+
+
+
+
+ 2
+ 0
+
+
+
+ Qt::Orientation::Vertical
+
+
+
+ Fit Summary
+
+
+ -
+
+
+ false
+
+
+
+ Property
+
+
+
+
+ Value
+
+
+
+
+
+
+
+
+ Parameter Details
+
+
+ -
+
+
+
+ Parameter
+
+
+
+
+ Value
+
+
+
+
+ Std
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bec_widgets/widgets/lmfit_dialog/lmfit_dialog_vertical.ui b/bec_widgets/widgets/lmfit_dialog/lmfit_dialog_vertical.ui
new file mode 100644
index 00000000..616e5d56
--- /dev/null
+++ b/bec_widgets/widgets/lmfit_dialog/lmfit_dialog_vertical.ui
@@ -0,0 +1,147 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 274
+ 568
+
+
+
+
+ 0
+ 0
+
+
+
+ Form
+
+
+ -
+
+
+ Select Curve
+
+
+
-
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 250
+ 200
+
+
+
+ Fit Summary
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ false
+
+
+ 80
+
+
+
+ Property
+
+
+
+
+ Value
+
+
+
+
+
+
+
+ -
+
+
+
+ 0
+ 0
+
+
+
+
+ 250
+ 200
+
+
+
+ Parameter Details
+
+
+
-
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+ 80
+
+
+
+ Parameter
+
+
+
+
+ Value
+
+
+
+
+ Std
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/bec_widgets/widgets/lmfit_dialog/register_lm_fit_dialog.py b/bec_widgets/widgets/lmfit_dialog/register_lm_fit_dialog.py
new file mode 100644
index 00000000..73f9c3aa
--- /dev/null
+++ b/bec_widgets/widgets/lmfit_dialog/register_lm_fit_dialog.py
@@ -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()
diff --git a/bec_widgets/widgets/waveform/waveform_popups/dap_summary_dialog/dap_summary.ui b/bec_widgets/widgets/waveform/waveform_popups/dap_summary_dialog/dap_summary.ui
deleted file mode 100644
index 49160460..00000000
--- a/bec_widgets/widgets/waveform/waveform_popups/dap_summary_dialog/dap_summary.ui
+++ /dev/null
@@ -1,127 +0,0 @@
-
-
- Form
-
-
-
- 0
- 0
- 800
- 600
-
-
-
- Form
-
-
- -
-
-
-
- 1
- 0
-
-
-
- QFrame::Shape::VLine
-
-
- QFrame::Shadow::Plain
-
-
- 1
-
-
- Qt::Orientation::Horizontal
-
-
- true
-
-
- true
-
-
-
- Select Curve
-
-
-
-
-
-
- Refresh DAP Summary
-
-
-
- -
-
-
-
-
-
-
-
- 2
- 0
-
-
-
- Qt::Orientation::Vertical
-
-
-
- Fit Summary
-
-
- -
-
-
- false
-
-
-
- Property
-
-
-
-
- Value
-
-
-
-
-
-
-
-
- Parameter Details
-
-
- -
-
-
-
- Parameter
-
-
-
-
- Value
-
-
-
-
- Std
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/bec_widgets/widgets/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py b/bec_widgets/widgets/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py
index 00512e05..54dd48d1 100644
--- a/bec_widgets/widgets/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py
+++ b/bec_widgets/widgets/waveform/waveform_popups/dap_summary_dialog/dap_summary_dialog.py
@@ -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)
diff --git a/bec_widgets/widgets/waveform/waveform_widget.py b/bec_widgets/widgets/waveform/waveform_widget.py
index 2f265494..c7c4cd5d 100644
--- a/bec_widgets/widgets/waveform/waveform_widget.py
+++ b/bec_widgets/widgets/waveform/waveform_widget.py
@@ -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:
diff --git a/docs/assets/widget_screenshots/lmfit_dialog.png b/docs/assets/widget_screenshots/lmfit_dialog.png
new file mode 100644
index 00000000..86b6203a
Binary files /dev/null and b/docs/assets/widget_screenshots/lmfit_dialog.png differ
diff --git a/docs/assets/widget_screenshots/lmfit_dialog_connect.png b/docs/assets/widget_screenshots/lmfit_dialog_connect.png
new file mode 100644
index 00000000..d2efa904
Binary files /dev/null and b/docs/assets/widget_screenshots/lmfit_dialog_connect.png differ
diff --git a/docs/user/widgets/lmfit_dialog/lmfit_dialog.md b/docs/user/widgets/lmfit_dialog/lmfit_dialog.md
new file mode 100644
index 00000000..29141d66
--- /dev/null
+++ b/docs/user/widgets/lmfit_dialog/lmfit_dialog.md
@@ -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
+```
+````
+
+
+
+
+
+
+
+
+
diff --git a/docs/user/widgets/widgets.md b/docs/user/widgets/widgets.md
index ce46b49d..86d353e0 100644
--- a/docs/user/widgets/widgets.md
+++ b/docs/user/widgets/widgets.md
@@ -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
```
\ No newline at end of file
diff --git a/tests/unit_tests/test_lmfit_dialog.py b/tests/unit_tests/test_lmfit_dialog.py
new file mode 100644
index 00000000..f016d353
--- /dev/null
+++ b/tests/unit_tests/test_lmfit_dialog.py
@@ -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"
diff --git a/tests/unit_tests/test_waveform_widget.py b/tests/unit_tests/test_waveform_widget.py
index abe31e8a..1446e015 100644
--- a/tests/unit_tests/test_waveform_widget.py
+++ b/tests/unit_tests/test_waveform_widget.py
@@ -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
###################################