GUI: Fluorescence split between panel and plot

This commit is contained in:
2025-11-13 10:40:12 +01:00
parent 4833b06829
commit 5ca8ad9f64
4 changed files with 187 additions and 107 deletions
+3 -3
View File
@@ -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):
@@ -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()
@@ -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))
+139 -104
View File
@@ -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()
logger.error(f"Update plot error: {e}")