From 4f3977cddb968cef6d7904bbd94c1674738b81c7 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Fri, 13 Jun 2025 16:22:41 +0200 Subject: [PATCH] GUI: One can choose metric for grid scan --- daq/src/aaredaq/daq.py | 5 ++- daq/src/aaredaq/server.py | 7 ++- gui/src/aaregui/main_window.py | 5 ++- .../aaregui/panels/raster_data_collection.py | 28 +++++++++--- .../aaregui/scan_logic/raster_grid_manager.py | 43 ++++++++++++++++--- gui/src/aaregui/threads/daq_worker.py | 38 +++++++++++++++- gui/src/aaregui/widgets/camera_image.py | 11 +++-- 7 files changed, 114 insertions(+), 23 deletions(-) diff --git a/daq/src/aaredaq/daq.py b/daq/src/aaredaq/daq.py index 4f13a210..ec6915e4 100644 --- a/daq/src/aaredaq/daq.py +++ b/daq/src/aaredaq/daq.py @@ -14,7 +14,7 @@ from aaredaqlib.models import ( PuckLoadedInfo, SampleShortInfoList, DAQStatusModel, BeamlineStatus, SessionStatus, SampleCameraSettings, AutofocusSettings, ) -from aaredaqlib.raster_grid import RasterGridRequest +from aaredaqlib.raster_grid import RasterGridRequest, CompletedRasterGrid from aaredaqlib.rotation_scan import RotationScanRequest from aaredaqlib.sample_geometry import SampleGeometryModel @@ -252,13 +252,14 @@ class AareDAQ: self.__devs.aerotech.reset() self.__devs.smargon.target = save_smargon_position - def measure_raster(self, r: RasterGridRequest): + def measure_raster(self, r: RasterGridRequest) -> CompletedRasterGrid: max_time = r.exp_time_s * r.n_y * r.n_x + 60 self.__cfg.try_set_busy(timeout=ceil(max_time + 360)) try: self.__raster(r) self.__set_state(BeamlineStateEnum.SampleAlignment) self.__cfg.state_busy = False + return CompletedRasterGrid(request=r) except Exception as e: self.__cfg.state_busy = False raise e diff --git a/daq/src/aaredaq/server.py b/daq/src/aaredaq/server.py index 5b1f84cb..171dbe42 100644 --- a/daq/src/aaredaq/server.py +++ b/daq/src/aaredaq/server.py @@ -7,7 +7,7 @@ import uvicorn from aaredaqlib.coordinate import SmargonCoordinate, Coordinate from aaredaqlib.models import SampleShortInfo, DAQStatusModel, BeamlineStateEnum, BeamlineSettingsModel, \ SampleShortInfoList, SessionStatus, SampleCameraSettings, AutofocusSettings -from aaredaqlib.raster_grid import RasterGridRequest +from aaredaqlib.raster_grid import RasterGridRequest, CompletedRasterGrid from aaredaqlib.rotation_scan import RotationScanRequest from aaredaqlib.sample_geometry import SampleGeometryModel from fastapi import FastAPI, Depends @@ -232,10 +232,9 @@ async def sample_alignment(token: str = Depends(oauth2_scheme)): # Scans @app.post("/scan/raster") -async def raster(val: RasterGridRequest, token: str = Depends(oauth2_scheme)): +async def raster(val: RasterGridRequest, token: str = Depends(oauth2_scheme)) -> CompletedRasterGrid: auth.check_jwt_rw(cfg, auth.parse_token(token)) - daq.measure_raster(val) - return "OK" + return daq.measure_raster(val) @app.post("/scan/rotation") diff --git a/gui/src/aaregui/main_window.py b/gui/src/aaregui/main_window.py index 11ff73b4..643a0a10 100644 --- a/gui/src/aaregui/main_window.py +++ b/gui/src/aaregui/main_window.py @@ -138,6 +138,8 @@ class MainWindow(QMainWindow): self.daq.update.connect(self.beamline.abr_tweak.update_daq_status) self.daq.update.connect(self.status_bar.update_daq_status) self.daq.update.connect(self.beamline.samcam.update_daq_status) + self.daq.raster_scan_completed.connect(self.raster.grid_scan_completed) + self.beamline.samcam.changed.connect(self.daq.samcam_settings) self.beamline.state_panel.sample_exchange.clicked.connect(self.daq.sample_exchange) self.beamline.state_panel.sample_alignment.clicked.connect(self.daq.sample_alignment) @@ -179,6 +181,7 @@ class MainWindow(QMainWindow): self.camera_image.evaluate_grid.connect(self.raster.run_grid_scan) self.data_collection.raster.evaluate_grid.connect(self.raster.run_grid_scan) + self.camera_image.clear_evaluated_grids.connect(self.raster.clear_completed_grids) self.camera_image.clear_grid.connect(self.raster.clear_active_grid) self.daq.run_number_incremented.connect(self.data_collection.file_path_panel.increment_run_number) @@ -193,7 +196,7 @@ class MainWindow(QMainWindow): self.data_collection.raster.exp_time_updated.connect(self.raster.update_exposure_time) self.data_collection.raster.transmission_updated.connect(self.raster.update_transmission) self.data_collection.raster.dtz_updated.connect(self.raster.update_dtz) - + self.data_collection.raster.grid_metric_updated.connect(self.raster.metric) self.raster.grid_scan.connect(self.daq.raster_scan) self.data_collection.screening.rotation_scan.connect(self.daq.standard_scan) diff --git a/gui/src/aaregui/panels/raster_data_collection.py b/gui/src/aaregui/panels/raster_data_collection.py index 54af6a0c..68ad6b02 100644 --- a/gui/src/aaregui/panels/raster_data_collection.py +++ b/gui/src/aaregui/panels/raster_data_collection.py @@ -1,9 +1,10 @@ from PySide6.QtCore import Signal, Slot, Qt -from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QSizePolicy, QSpacerItem, QPushButton, QHBoxLayout +from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QSizePolicy, QSpacerItem, QPushButton, QHBoxLayout, \ + QComboBox from aaredaqlib.diffraction_geometry import DiffractionGeometry from aaregui.panels.scan_settings_panel import ScanSettingsPanel -from aaregui.scan_logic.raster_grid_manager import RasterGridManager +from aaregui.scan_logic.raster_grid_manager import RasterGridManager, RasterGridMetric from aaregui.widgets.number_line_edit import NumberLineEdit @@ -11,6 +12,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel): grid_size_updated = Signal(float, float) exp_time_updated = Signal(float) evaluate_grid = Signal() + grid_metric_updated = Signal(RasterGridMetric) def __init__(self, raster_mgr :RasterGridManager, diffraction: DiffractionGeometry, @@ -72,15 +74,28 @@ class RasterDataCollectionPanel(ScanSettingsPanel): self._layout.addWidget(QLabel("μm", parent=self), 6, 4) + self.metric_combo = QComboBox() + self._layout.addWidget(QLabel("Metric", parent=self), 7, 0) + + self.metric_combo.addItem("Background estimate", RasterGridMetric.BKG) + self.metric_combo.addItem("Spot count", RasterGridMetric.SPOTS) + self.metric_combo.addItem("Indexing result", RasterGridMetric.INDEXING) + self.metric_combo.addItem("Mosaicity", RasterGridMetric.MOS) + self.metric_combo.addItem("B-factor", RasterGridMetric.BFACTOR) + + self._layout.addWidget(self.metric_combo, 7, 1, 1, 3) + + self.metric_combo.currentIndexChanged.connect(self.metric_changed) + horizontal_spacer = QSpacerItem( 40, 20, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding ) - self._layout.addItem(horizontal_spacer, 7, 0, 1, 4) + self._layout.addItem(horizontal_spacer, 8, 0, 1, 4) self.start_button = QPushButton("Evaluate grid") self.start_button.setStyleSheet("color: rgb(78, 154, 6);") self.start_button.clicked.connect(self.evaluate_grid) - self._layout.addWidget(self.start_button, 8, 0, 1, 5) + self._layout.addWidget(self.start_button, 9, 0, 1, 5) self.update_grid_scan_size() @@ -106,4 +121,7 @@ class RasterDataCollectionPanel(ScanSettingsPanel): self.n_x_label.setText(str(self.__n_x)) self.size_x_label.setText(f"{self.__size_x * self.__n_x:.1f}") self.n_y_label.setText(str(self.__n_y)) - self.size_y_label.setText(f"{self.__size_y * self.__n_y:.1f}") \ No newline at end of file + self.size_y_label.setText(f"{self.__size_y * self.__n_y:.1f}") + + def metric_changed(self, _: int): + self.grid_metric_updated.emit(self.metric_combo.currentData()) \ No newline at end of file diff --git a/gui/src/aaregui/scan_logic/raster_grid_manager.py b/gui/src/aaregui/scan_logic/raster_grid_manager.py index 3a0c9181..37cb41e5 100644 --- a/gui/src/aaregui/scan_logic/raster_grid_manager.py +++ b/gui/src/aaregui/scan_logic/raster_grid_manager.py @@ -1,4 +1,5 @@ import math +from enum import Enum from PySide6.QtCore import QObject, Signal, Slot, QPointF from PySide6.QtGui import QPainter, QPen, QColor, QBrush @@ -9,6 +10,17 @@ from aaredaqlib.coordinate import Coordinate, SmargonCoordinate from aaredaqlib.models import DAQStatusModel from aaredaqlib.raster_grid import RasterGridRequest, CompletedRasterGrid +class RasterGridMetric(Enum): + BKG = 0 + SPOTS = 1 + INDEXING = 2 + MOS = 3 + BFACTOR = 4 + +class GridPointInfo: + filename: str + det_img: int + bkg_estimate: float def float_to_viridis_brush(value: float, alpha: int = 127) -> QBrush: """ @@ -65,6 +77,8 @@ class RasterGridManager(QObject): super().__init__() self.__geom = geom + self.__metric = RasterGridMetric.SPOTS + self.__start_point : QPointF = QPointF(0, 0) self.__active_grid : RasterGridRequest = RasterGridRequest( n_x= 0, @@ -188,25 +202,38 @@ class RasterGridManager(QObject): def draw_grid(self, painter: QPainter): self._draw_grid(painter, self.__active_grid) for i in self.__completed_grids: - self._draw_grid(painter, i.request, i.spot_count) + v = None + match self.__metric: + case RasterGridMetric.BKG: + v = i.bkg_estimate + case RasterGridMetric.SPOTS: + v = i.spot_count + case RasterGridMetric.INDEXING: + v = i.indexed + case RasterGridMetric.MOS: + v = i.mosaicity + case RasterGridMetric.BFACTOR: + v = i.b_factor + self._draw_grid(painter, i.request, v) def _draw_grid(self, painter: QPainter, grid: RasterGridRequest, values: List[float] | List[int] | None = None): if not self._is_grid_visible(grid): return - painter.setPen(QPen(QColor(114, 159, 207), 2, Qt.PenStyle.SolidLine)) if values is not None: + painter.setPen(Qt.PenStyle.NoPen) min_value = min((x for x in values if not math.isnan(x)), default=0) max_value = max((x for x in values if not math.isnan(x)), default=1) else: + painter.setPen(QPen(QColor(114, 159, 207), 2, Qt.PenStyle.SolidLine)) min_value = 0 max_value = 1 g = grid.grid_size_pxl(self.__geom) c0 = grid.start_pxl(self.__geom) - for x in range(self.__active_grid.n_x): - for y in range(self.__active_grid.n_y): - pxl = x + y * self.__active_grid.n_x + for x in range(grid.n_x): + for y in range(grid.n_y): + pxl = x + y * grid.n_x if values is None or len(values) <= pxl or math.isnan(values[pxl]): painter.setBrush(Qt.BrushStyle.NoBrush) else: @@ -230,4 +257,8 @@ class RasterGridManager(QObject): @Slot(str) def update_filename(self, filename: str): - self.__active_grid.file_prefix = filename \ No newline at end of file + self.__active_grid.file_prefix = filename + + @Slot(RasterGridMetric) + def metric(self, r: RasterGridMetric): + self.__metric = r diff --git a/gui/src/aaregui/threads/daq_worker.py b/gui/src/aaregui/threads/daq_worker.py index 89005760..8f7ec07b 100644 --- a/gui/src/aaregui/threads/daq_worker.py +++ b/gui/src/aaregui/threads/daq_worker.py @@ -1,11 +1,15 @@ +import copy +import random + from PySide6.QtCore import Signal, QUrl, Slot, QTimer, QObject, QByteArray from PySide6.QtNetwork import QNetworkAccessManager, QNetworkRequest, QNetworkReply from aaredaqlib.models import DAQStatusModel, SampleShortInfoList, SampleShortInfo, SampleCameraSettings, \ AutofocusSettings from aaredaqlib.coordinate import SmargonCoordinate, Coordinate -from aaredaqlib.raster_grid import RasterGridRequest +from aaredaqlib.raster_grid import RasterGridRequest, CompletedRasterGrid from aaredaqlib.rotation_scan import RotationScanRequest +import numpy as np SPREADHSEET_FREQUENCY = 25 # Every 5 seconds @@ -17,6 +21,7 @@ class DAQWorker(QObject): auth_error = Signal() automated_scan_done = Signal(int, bool) # sample ID, success run_number_incremented = Signal() + raster_scan_completed = Signal(CompletedRasterGrid) def __init__(self, base_url: str | None, token: str, parent=None): super().__init__(parent) @@ -186,10 +191,39 @@ class DAQWorker(QObject): self.run_number_incremented.emit() self.generic_post("scan/rotation", r.model_dump_json()) + def handle_raster_scan_response(self, reply: QNetworkReply): + try: + response_data = self.handle_response(reply) + parsed_response = CompletedRasterGrid.model_validate_json(response_data) + self.raster_scan_completed.emit(parsed_response) + except Exception as e: + print(e) + self.http_error.emit(str(e)) + @Slot(RasterGridRequest) def raster_scan(self, r: RasterGridRequest): self.run_number_incremented.emit() - self.generic_post("scan/raster", r.model_dump_json()) + if self.__base_url is None: + print(f"POST /scan/raster: {r.model_dump_json()}") + image_number = r.get_image_number() + new_copy = copy.deepcopy(r) + + reply = CompletedRasterGrid(request=new_copy, + bkg_estimate=[random.gauss(3.0, 0.1) for _ in range(image_number)], + spot_count=[random.randint(0, 250) for _ in range(image_number)], + indexed=[random.randint(0, 1) for _ in range(image_number)], + mosaicity=[random.uniform(0, 0.1) for _ in range(image_number)], + b_factor=[random.uniform(15.0, 80.0) for _ in range(image_number)]) + + self.raster_scan_completed.emit(reply) + return + + request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/raster")) + request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) + request.setRawHeader(b"Content-Type", b"application/json") + body = r.model_dump_json() + reply = self.__net_manager.post(request, QByteArray(body.encode("utf-8"))) + reply.finished.connect(lambda: self.handle_raster_scan_response(reply)) @Slot() def load_spreadsheet(self): diff --git a/gui/src/aaregui/widgets/camera_image.py b/gui/src/aaregui/widgets/camera_image.py index b05e992a..cbb62406 100644 --- a/gui/src/aaregui/widgets/camera_image.py +++ b/gui/src/aaregui/widgets/camera_image.py @@ -41,6 +41,7 @@ class SampleCameraImageLabel(QGraphicsView): evaluate_grid = Signal() clear_grid = Signal() + clear_evaluated_grids = Signal() zoom_change = Signal(int) set_omega = Signal(float) @@ -194,10 +195,12 @@ class SampleCameraImageLabel(QGraphicsView): scene_pos = self.mapToScene(event.pos()) + menu.addSection("Grid") + if self.__raster_mgr.is_part_of_active_grid(scene_pos): - menu.addSection("Grid") - delete_action = menu.addAction("Delete") - evaluate_action = menu.addAction("Evaluate") + delete_action = menu.addAction("Delete grid") + evaluate_action = menu.addAction("Evaluate grid") + delete_completed_action = menu.addAction("Delete completed grids") action = menu.exec_(self.mapToGlobal(event.pos())) @@ -212,6 +215,8 @@ class SampleCameraImageLabel(QGraphicsView): self.clear_grid.emit() elif action == evaluate_action: self.evaluate_grid.emit() + elif action == delete_completed_action: + self.clear_evaluated_grids.emit() elif action == grab_action: self.__screenshot_with_dialog(overlay=False) elif action == grab_with_overlay_action: