GUI: Fluorescence split between panel and plot
This commit is contained in:
@@ -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))
|
||||
@@ -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}")
|
||||
Reference in New Issue
Block a user