From 5ca8ad9f644eb03926487536427107fbd65ab2d6 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Thu, 13 Nov 2025 10:40:12 +0100 Subject: [PATCH] GUI: Fluorescence split between panel and plot --- gui/src/aaregui/main_window.py | 6 +- .../panels/data_collection_settings.py | 4 + .../panels/fluorescence_data_collection.py | 41 +++ gui/src/aaregui/panels/fluoresence_panel.py | 243 ++++++++++-------- 4 files changed, 187 insertions(+), 107 deletions(-) create mode 100644 gui/src/aaregui/panels/fluorescence_data_collection.py diff --git a/gui/src/aaregui/main_window.py b/gui/src/aaregui/main_window.py index f52bffd1..83251ea3 100644 --- a/gui/src/aaregui/main_window.py +++ b/gui/src/aaregui/main_window.py @@ -190,8 +190,8 @@ class MainWindow(QMainWindow): self.fluor_panel = FluorescencePanel() self.fluor_panel_dock = QDockWidget("Fluorescence", self) self.fluor_panel_dock.setWidget(self.fluor_panel) - self.fluor_panel_dock.setAllowedAreas(Qt.DockWidgetArea.RightDockWidgetArea | Qt.DockWidgetArea.LeftDockWidgetArea) - self.addDockWidget(Qt.DockWidgetArea.RightDockWidgetArea, self.fluor_panel_dock) + self.fluor_panel_dock.setAllowedAreas(Qt.DockWidgetArea.TopDockWidgetArea | Qt.DockWidgetArea.BottomDockWidgetArea | Qt.DockWidgetArea.RightDockWidgetArea) + self.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.fluor_panel_dock) self.fluor_panel_dock.hide() # Create and add the dock to your main window @@ -356,7 +356,7 @@ class MainWindow(QMainWindow): self.status_bar.get_all_pgroups.connect(self.daq.get_all_pgroups) self.daq.staff_pgroups_loaded.connect(self.status_bar.staff_pgroups_loaded) - self.fluor_panel.spectrum.connect(self.daq.fluorimeter_spectrum) + self.data_collection.fluo.fluo_scan.connect(self.daq.fluorimeter_spectrum) self.daq.fluorimeter_spectrum_update.connect(self.fluor_panel.update_plot) def create_menu_bar(self): diff --git a/gui/src/aaregui/panels/data_collection_settings.py b/gui/src/aaregui/panels/data_collection_settings.py index 9bdc1432..61728a29 100644 --- a/gui/src/aaregui/panels/data_collection_settings.py +++ b/gui/src/aaregui/panels/data_collection_settings.py @@ -14,6 +14,7 @@ from aaredaqlib.rotation_scan import RotationScanRequest from aaredaqlib.sample_geometry import SampleGeometryModel from aaregui.panels.file_path_panel import FilePathPanel +from aaregui.panels.fluorescence_data_collection import FluorescenceDataCollectionPanel from aaregui.panels.raster_data_collection import RasterDataCollectionPanel from aaregui.panels.rotation_data_collection import RotationDataCollectionPanel from aaregui.panels.smart_rotation_panel import SimpleRotationSettingsPanel @@ -50,6 +51,9 @@ class DataCollectionSettings(QFrame): self.simple = SimpleRotationSettingsPanel(parent=self) self.__tab_widget.addTab(self.simple, "Simple") + self.fluo = FluorescenceDataCollectionPanel(parent=self) + self.__tab_widget.addTab(self.fluo, "XRF") + v_layout.addWidget(self.__tab_widget) v_layout.addStretch() diff --git a/gui/src/aaregui/panels/fluorescence_data_collection.py b/gui/src/aaregui/panels/fluorescence_data_collection.py new file mode 100644 index 00000000..1baaa27e --- /dev/null +++ b/gui/src/aaregui/panels/fluorescence_data_collection.py @@ -0,0 +1,41 @@ +from PySide6.QtCore import Signal, Slot +from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QPushButton + +from aaredaqlib.models import FluorescenceSpectrumParameterModel +from aaregui.widgets.number_line_edit import NumberLineEdit + + +class FluorescenceDataCollectionPanel(QWidget): + fluo_scan = Signal(FluorescenceSpectrumParameterModel) + + def __init__(self, parent=None): + super().__init__(parent) + + lay = QGridLayout(self) + + # Beam transmission (0..1) + lay.addWidget(QLabel("Beam transmission", self), 0, 0) + self.transmission = NumberLineEdit(0.0, 1.0, decimals=4, default=0.1, parent=self) + lay.addWidget(self.transmission, 0, 1) + + # Exposure time (seconds) + lay.addWidget(QLabel("Exposure time", self), 1, 0) + self.exposure = NumberLineEdit(0.01, 60.0, decimals=3, default=1.0, parent=self) + lay.addWidget(self.exposure, 1, 1) + lay.addWidget(QLabel("s", self), 1, 2) + + # Add vertical stretch + lay.setRowStretch(2, 1) + + # Run button + self.run_btn = QPushButton("Run fluorescence", self) + self.run_btn.setStyleSheet("color: rgb(78, 154, 6);") + lay.addWidget(self.run_btn, 3, 0, 1, 3) + + self.run_btn.clicked.connect(self._emit_params) + + @Slot() + def _emit_params(self): + t = float(self.transmission.value) + exp = float(self.exposure.value) + self.fluo_scan.emit(FluorescenceSpectrumParameterModel(acq_time_s=exp, transmission=t)) diff --git a/gui/src/aaregui/panels/fluoresence_panel.py b/gui/src/aaregui/panels/fluoresence_panel.py index 9bbe8274..51741f92 100644 --- a/gui/src/aaregui/panels/fluoresence_panel.py +++ b/gui/src/aaregui/panels/fluoresence_panel.py @@ -1,126 +1,161 @@ -from PySide6.QtCore import Slot, QTimer, Signal -from PySide6.QtWidgets import QWidget, QGridLayout, QPushButton -from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas -from matplotlib.figure import Figure import numpy as np +from PySide6.QtCharts import QChart, QChartView, QLineSeries, QValueAxis +from PySide6.QtCore import QPointF, Qt, Slot, Signal, QEvent +from PySide6.QtGui import QPainter, QColor, QPen +from PySide6.QtWidgets import QWidget, QGridLayout, QGraphicsSimpleTextItem -from aaredaqlib.models import FluorescenceSpectrumParameterModel, FluorescenceSpectrumOutputModel -from aaregui.widgets.number_line_edit import NumberLineEdit -from aaregui.widgets.title_label import TitleLabel from aaredaqlib.logger_config import setup_logger +from aaredaqlib.models import FluorescenceSpectrumParameterModel, FluorescenceSpectrumOutputModel logger = setup_logger(__name__) class FluorescencePanel(QWidget): - spectrum = Signal(FluorescenceSpectrumParameterModel) - def __init__(self, parent=None): super().__init__(parent) grid = QGridLayout(self) - grid.addWidget(TitleLabel("Fluorescence", self), 0, 0) - self.btn_start = QPushButton("Spectrum") - grid.addWidget(self.btn_start, 1, 0) + # Replace Matplotlib with Qt Charts + self.series = QLineSeries() + self.chart = QChart() + self.chart.addSeries(self.series) + self.chart.legend().hide() + self.chart.setTitle("Fluorescence Spectrum") - self.fig = Figure(figsize=(5, 3), tight_layout=True) - self.ax = self.fig.add_subplot(111) - self.canvas = FigureCanvas(self.fig) - grid.addWidget(self.canvas, 2, 0) + # Axes + self.axis_x = QValueAxis() + self.axis_x.setTitleText("Energy [keV]") + self.axis_y = QValueAxis() + self.axis_y.setTitleText("Counts") + self.chart.addAxis(self.axis_x, Qt.AlignBottom) + self.chart.addAxis(self.axis_y, Qt.AlignLeft) + self.series.attachAxis(self.axis_x) + self.series.attachAxis(self.axis_y) - # Wire buttons - self.btn_start.clicked.connect(lambda: self.spectrum.emit(FluorescenceSpectrumParameterModel(acq_time_s=0.1))) + # Chart view + self.chart_view = QChartView(self.chart) + self.chart_view.setRenderHint(QPainter.Antialiasing) + grid.addWidget(self.chart_view, 0, 0) + + # Popup label for clicked point (use scene item instead of chart.addText) + self._marker = QGraphicsSimpleTextItem("") + self._marker.setVisible(False) + self.chart.scene().addItem(self._marker) + + # Vertical marker line (two-point series) + self._vline = QLineSeries() + pen = QPen(QColor("#cc0000")) + pen.setWidth(2) + self._vline.setPen(pen) + self.chart.addSeries(self._vline) + self._vline.attachAxis(self.axis_x) + self._vline.attachAxis(self.axis_y) + self._vline.setVisible(False) + + # Enable mouse tracking for hover readout + self.chart_view.setMouseTracking(True) + self.chart_view.viewport().setMouseTracking(True) + self.chart_view.viewport().installEventFilter(self) + + def eventFilter(self, obj, event): + try: + if obj is self.chart_view.viewport() and event.type() == QEvent.Type.MouseMove: + pos = event.position() if hasattr(event, "position") else event.pos() + p = QPointF(pos.x(), pos.y()) + plot = self.chart.plotArea() + if not plot.contains(p) or self.series.count() == 0: + self.chart_view.setToolTip("") + self._vline.setVisible(False) + return False + # Map pixel X -> chart X + x_val = self.axis_x.min() + (self.axis_x.max() - self.axis_x.min()) * ( + (p.x() - plot.left()) / plot.width()) + + # Snap to the largest Y within +/- 3 indices around nearest index + center = self._nearest_index(x_val) + n = self.series.count() + left = max(0, center - 10) + right = min(n - 1, center + 10) + + best_i = left + best_y = self.series.at(best_i).y() + for i in range(left + 1, right + 1): + yi = self.series.at(i).y() + if yi > best_y: + best_y = yi + best_i = i + + pt = self.series.at(best_i) + self.chart_view.setToolTip(f"Energy {pt.x():.3f} keV counts {pt.y():.3f}") + self._update_vline(pt.x()) + return False + except Exception as e: + logger.debug(f"eventFilter error: {e}") + return False + + def _update_vline(self, x_val: float): + # Draw vertical line spanning current Y axis range at x_val + try: + ymin = self.axis_y.min() + ymax = self.axis_y.max() + self._vline.replace(0, x_val, ymin) if self._vline.count() > 0 else self._vline.append(x_val, ymin) + if self._vline.count() == 1: + self._vline.append(x_val, ymax) + else: + self._vline.replace(1, x_val, ymax) + self._vline.setVisible(True) + except Exception as e: + logger.debug(f"vline error: {e}") + + def _nearest_index(self, x_val: float) -> int: + n = self.series.count() + if n == 0: + return 0 + lo, hi = 0, n - 1 + while lo < hi: + mid = (lo + hi) // 2 + if self.series.at(mid).x() < x_val: + lo = mid + 1 + else: + hi = mid + cand = [max(0, lo - 1), lo] + best_i = cand[0] + best_d = abs(self.series.at(best_i).x() - x_val) + for i in cand[1:]: + d = abs(self.series.at(i).x() - x_val) + if d < best_d: + best_i, best_d = i, d + return best_i @Slot(FluorescenceSpectrumOutputModel) def update_plot(self, f: FluorescenceSpectrumOutputModel): try: - self.ax.clear() + self.series.clear() + self._marker.setVisible(False) + self._vline.setVisible(False) + self._vline.clear() + if len(f.spectrum) > 0 and len(f.spectrum) == len(f.energy_eV): - x = np.array(f.energy_eV, dtype=float) + x = np.array(f.energy_eV, dtype=float) / 1000.0 # keV y = np.array(f.spectrum, dtype=float) - self.ax.plot(x, y, lw=1.0, label="Data") - self.ax.set_xlabel("Energy eV") - self.ax.set_ylabel("Counts") - self.ax.grid(True, alpha=0.3) - self.canvas.draw_idle() + # Ensure sorted by X for binary search + order = np.argsort(x) + x = x[order] + y = y[order] + + for xi, yi in zip(x, y): + self.series.append(float(xi), float(yi)) + + xmin = float(np.min(x)) if x.size else -1.0 + xmax = float(np.max(x)) if x.size else 1.0 + ymin = float(np.min(y)) if y.size else 0.0 + ymax = float(np.max(y)) if y.size else 1.0 + if xmin == xmax: + xmax = xmin + 1.0 + if ymin == ymax: + ymax = ymin + 1.0 + self.axis_x.setRange(xmin, xmax) + self.axis_y.setRange(ymin, ymax) except Exception as e: - logger.error(f"Update plot error: {e}") - - -# class FluorescencePanel(QWidget): -# def __init__(self, daq_worker, parent=None): -# super().__init__(parent) -# self._w = daq_worker -# -# lay = QGridLayout(self) -# self.btn_start = QPushButton("Start") -# self.btn_start_erase = QPushButton("Erase+Start") -# self.btn_stop = QPushButton("Stop") -# self.btn_live = QPushButton("Start Live") -# self.btn_snapshot = QPushButton("Snapshot") -# lay.addWidget(self.btn_start, 0, 0) -# lay.addWidget(self.btn_start_erase, 0, 1) -# lay.addWidget(self.btn_stop, 0, 2) -# lay.addWidget(self.btn_live, 0, 3) -# lay.addWidget(self.btn_snapshot, 0, 4) -# -# self.fig = Figure(figsize=(5, 3), tight_layout=True) -# self.ax = self.fig.add_subplot(111) -# self.canvas = FigureCanvas(self.fig) -# lay.addWidget(self.canvas, 1, 0, 1, 5) -# -# # Polling timer -# self._timer = QTimer(self) -# self._timer.setInterval(200) -# self._timer.timeout.connect(self._request_snapshot) -# -# # Wire buttons -# self.btn_start.clicked.connect(self._w.fluorimeter_start) -# self.btn_start_erase.clicked.connect(self._w.fluorimeter_start_erase) -# self.btn_stop.clicked.connect(self._on_stop_clicked) -# self.btn_snapshot.clicked.connect(self._request_snapshot) -# self.btn_live.clicked.connect(self._on_start_live) -# -# # Subscribe to data updates -# # Expected signal: fluorimeter_update(list data, list background, int status) -# self._w.fluorimeter_update.connect(self.update_plot) -# -# @Slot() -# def _on_start_live(self): -# # Start acquisition if not already running, then begin polling -# self._w.fluorimeter_start() -# self._timer.start() -# -# @Slot() -# def _on_stop_clicked(self): -# self._timer.stop() -# self._w.fluorimeter_stop() -# -# @Slot() -# def _request_snapshot(self): -# # One-shot HTTP GET to fetch data+status+background (worker emits fluorimeter_update) -# self._w.fluorimeter_request_snapshot() -# -# @Slot(list, list, int) -# def update_plot(self, data, background, status): -# # Plot and stop timer automatically when acquisition ends -# self.ax.clear() -# if data: -# y = np.array(data, dtype=float) -# x = np.arange(len(y)) -# self.ax.plot(x, y, lw=1.0, label="Data") -# if background: -# b = np.array(background, dtype=float) -# xb = np.arange(len(b)) -# self.ax.plot(xb, b, lw=1.0, label="Background") -# if self.ax.has_data(): -# self.ax.legend(loc="best") -# self.ax.set_xlabel("Channel") -# self.ax.set_ylabel("Counts") -# self.ax.grid(True, alpha=0.3) -# self.canvas.draw_idle() -# -# # status: 1 acquiring, 0 done, other -> unknown -# if status == 0: -# self._timer.stop() \ No newline at end of file + logger.error(f"Update plot error: {e}") \ No newline at end of file