From 1d9c8133c18dfd1fc4f6f2cd61632c244ea36ec3 Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Thu, 26 Jun 2025 12:12:36 +0200 Subject: [PATCH] DAQ+GUI: Work in progress --- common/pyproject.toml | 2 +- common/src/aaredaqlib/raster_grid.py | 3 +- common/src/aaredaqlib/rotation_scan.py | 5 +++ daq/src/aaredaq/aaredb.py | 38 +++-------------- daq/src/aaredaq/daq.py | 29 +++++++++---- daq/src/aaredaq/server.py | 32 ++++++++++----- gui/src/aaregui/main_window.py | 12 ++++-- gui/src/aaregui/panels/manual_sample_panel.py | 41 +++++++++++++++++++ .../panels/rotation_data_collection.py | 2 +- .../aaregui/scan_logic/raster_grid_manager.py | 12 ++++-- .../scan_logic/rotation_scan_manager.py | 11 ++++- gui/src/aaregui/threads/daq_worker.py | 37 ++++++++++++----- 12 files changed, 151 insertions(+), 73 deletions(-) create mode 100644 gui/src/aaregui/panels/manual_sample_panel.py diff --git a/common/pyproject.toml b/common/pyproject.toml index a61a7c41..e586c797 100644 --- a/common/pyproject.toml +++ b/common/pyproject.toml @@ -8,7 +8,7 @@ dependencies = [ "pydantic==2.11.4", "numpy==2.2.5", "jfjoch_client==1.0.0rc51", - "aareDB==0.1.1a9" + "aareDB==0.1.1a11" ] [lint] diff --git a/common/src/aaredaqlib/raster_grid.py b/common/src/aaredaqlib/raster_grid.py index b3692cf2..655d9377 100644 --- a/common/src/aaredaqlib/raster_grid.py +++ b/common/src/aaredaqlib/raster_grid.py @@ -1,4 +1,4 @@ -from typing import Annotated, List +from typing import Annotated from jfjoch_client.models.scan_result import ScanResult from pydantic import Field, AfterValidator, BaseModel @@ -6,6 +6,7 @@ from pydantic import Field, AfterValidator, BaseModel from aaredaqlib.coordinate import Coordinate, positive_coords, SmargonCoordinate from aaredaqlib.sample_geometry import SampleGeometryModel + class RasterGridRequest(BaseModel): dtz: float | None = None transmission: float | None = None diff --git a/common/src/aaredaqlib/rotation_scan.py b/common/src/aaredaqlib/rotation_scan.py index a6c60f62..13885d12 100644 --- a/common/src/aaredaqlib/rotation_scan.py +++ b/common/src/aaredaqlib/rotation_scan.py @@ -1,3 +1,4 @@ +from jfjoch_client.models.scan_result import ScanResult from pydantic import BaseModel from aaredaqlib.coordinate import SmargonCoordinate @@ -18,3 +19,7 @@ class RotationScanRequest(BaseModel): screening: bool = False wedge_omega_deg: float | None = None + +class CompletedRotationScan(BaseModel): + request: RotationScanRequest + result: ScanResult \ No newline at end of file diff --git a/daq/src/aaredaq/aaredb.py b/daq/src/aaredaq/aaredb.py index 18178a6c..d5c57f44 100644 --- a/daq/src/aaredaq/aaredb.py +++ b/daq/src/aaredaq/aaredb.py @@ -67,47 +67,19 @@ class AareWrapper: ) print(ret) - def create_manual_sample( - self, - s: SampleShortInfo, - p: SessionStatus, - cellparameters: Optional[str] = None, - spacegroupnumber: Optional[int] = None, - ) -> Optional[object]: - """ - Insert a new manual sample into the database using a SampleShortInfo object. - - Args: - s (SampleShortInfo): The sample info object to extract data from. - p (SessionStatus): The session status object to extract data from. - cellparameters (Optional[str]): Unit cell parameters as a string. - spacegroupnumber (Optional[int]): Space group number. - - Returns: - API response object or None if there was an error. - """ - from aareDBclient.models import ManualSampleCreate, DataCollectionParameters - - data_collection_parameters = None - if cellparameters is not None and spacegroupnumber is not None: - data_collection_parameters = DataCollectionParameters( - cellparameters=cellparameters, - spacegroupnumber=spacegroupnumber - ) + def create_manual_sample(self, s: SampleShortInfo): + from aareDBclient.models import ManualSampleCreate manual_sample = ManualSampleCreate( - pgroup=p.current_pgroup, + pgroup=s.user, sample_name=s.sample_name, - data_collection_parameters=data_collection_parameters + data_collection_parameters=s.data_collection_parameters ) try: - result = self.__sample_api.insert_sample(manual_sample) - print(f"Sample created: {result}") - return result + s.db_id = self.__sample_api.insert_sample(manual_sample).id except Exception as e: print(f"Error inserting sample: {e}") - return None def sample_mounted(self, s: Optional[SampleShortInfo]): if s is not None: diff --git a/daq/src/aaredaq/daq.py b/daq/src/aaredaq/daq.py index 73785bd6..90cd6246 100644 --- a/daq/src/aaredaq/daq.py +++ b/daq/src/aaredaq/daq.py @@ -23,7 +23,7 @@ from aaredaqlib.models import ( SampleShortInfoList, DAQStatusModel, BeamlineStatus, SessionStatus, SampleCameraSettings, AutofocusSettings, ) from aaredaqlib.raster_grid import RasterGridRequest, CompletedRasterGrid -from aaredaqlib.rotation_scan import RotationScanRequest +from aaredaqlib.rotation_scan import RotationScanRequest, CompletedRotationScan from aaredaqlib.sample_geometry import SampleGeometryModel from mxlibs3.jfjoch import JFJochWrapper @@ -176,6 +176,16 @@ class AareDAQ: self.__cfg.state_busy = False raise + def create_sample(self, target: SampleShortInfo): + self.__aare.create_manual_sample(target) + self.__cfg.current_sample = target + + def clear_current_sample(self): + curr_sample = self.__cfg.current_sample + if curr_sample is not None: + self.__aare.sample_unmounted(curr_sample) + self.__cfg.current_sample = None + def __mount(self, target: SampleShortInfo | None): self.__set_state(BeamlineStateEnum.RobotSampleExchange) self.__devs.smargon.move_home(wait=True) @@ -192,8 +202,11 @@ class AareDAQ: curr_sample = self.__cfg.current_sample self.__devs.tell.wait_ready() - - if target is None: + if curr_sample is not None and curr_sample.location is None: + # TODO: Smarter way to know manual sample has be removed from goniometer + self.__aare.sample_unmounted(curr_sample) + self.__cfg.current_sample = None + elif target is None: self.__devs.tell.unmount(wait=True, timeout=360) self.__aare.sample_unmounted(curr_sample) self.__cfg.current_sample = None @@ -300,7 +313,7 @@ class AareDAQ: self.__cfg.state_busy = False raise e - def __rotation(self, request: RotationScanRequest): + def __rotation(self, request: RotationScanRequest) -> CompletedRotationScan: self.__cfg.dtz = request.dtz self.__set_state(BeamlineStateEnum.DataCollection) @@ -343,18 +356,20 @@ class AareDAQ: time.sleep(smargon_time_step) self.__devs.aerotech.wait_scan_done(total_time + 60) - self.__jfjoch.wait_till_done(60) self.__aare.sample_collected(self.sample) self.__devs.aerotech.reset() + result = self.__jfjoch.wait_till_done(60) + return CompletedRotationScan(request=copy.deepcopy(request), result=result) - def measure_rotation(self, request: RotationScanRequest): + def measure_rotation(self, request: RotationScanRequest) -> CompletedRotationScan: total_time = request.exp_time_s * request.steps self.__cfg.try_set_busy(timeout=ceil(total_time + 360)) try: - self.__rotation(request) + result = self.__rotation(request) self.__set_state(BeamlineStateEnum.SampleAlignment) self.__cfg.state_busy = False + return result except Exception as e: self.__set_state(BeamlineStateEnum.SampleAlignment) self.__cfg.state_busy = False diff --git a/daq/src/aaredaq/server.py b/daq/src/aaredaq/server.py index 74c8bf53..ecc176b3 100644 --- a/daq/src/aaredaq/server.py +++ b/daq/src/aaredaq/server.py @@ -9,7 +9,7 @@ from aaredaqlib.coordinate import SmargonCoordinate, Coordinate from aaredaqlib.models import SampleShortInfo, DAQStatusModel, BeamlineStateEnum, BeamlineSettingsModel, \ SampleShortInfoList, SessionStatus, SampleCameraSettings, AutofocusSettings, TokenData from aaredaqlib.raster_grid import RasterGridRequest, CompletedRasterGrid -from aaredaqlib.rotation_scan import RotationScanRequest +from aaredaqlib.rotation_scan import RotationScanRequest, CompletedRotationScan from aaredaqlib.sample_geometry import SampleGeometryModel from fastapi import FastAPI, Depends from fastapi import HTTPException @@ -162,7 +162,7 @@ async def get_image(token: str = Depends(oauth2_scheme)): # TELL procedures -@app.get("/tell/sample") +@app.get("/sample/curr_sample") async def sample(token: str = Depends(oauth2_scheme)) -> SampleShortInfo: token_data = auth.parse_token(token) auth.check_jwt_ro(cfg, auth.parse_token(token)) @@ -175,12 +175,12 @@ async def sample(token: str = Depends(oauth2_scheme)) -> SampleShortInfo: puck_name="", dewar_name="", db_id=-1, - pin=sample.pin, - location=sample.location + pin=s.pin, + location=s.location ) -@app.post("/tell/mount") +@app.post("/sample/mount") async def mount(dbid: int, token: str = Depends(oauth2_scheme)): token_data = auth.parse_token(token) auth.check_jwt_rw(cfg, auth.parse_token(token)) @@ -204,12 +204,25 @@ async def mount(dbid: int, token: str = Depends(oauth2_scheme)): ) -@app.post("/tell/unmount") +@app.post("/sample/unmount") async def unmount(token: str = Depends(oauth2_scheme)): auth.check_jwt_rw(cfg, auth.parse_token(token)) daq.sample = None return "OK" +@app.post("/sample/clear") +async def sample_clear(token: str = Depends(oauth2_scheme)): + auth.check_jwt_rw(cfg, auth.parse_token(token)) + daq.clear_current_sample() + return "OK" + +@app.post("/sample/manual") +async def manual(s: SampleShortInfo, token: str = Depends(oauth2_scheme)): + auth.check_jwt_rw(cfg, auth.parse_token(token)) + print(f"DB ID prior creating {s.db_id}") + daq.create_sample(s) + print(f"DB ID after creating {s.db_id}") + def get_spreadsheet(data: TokenData) -> SampleShortInfoList: if data.staff: @@ -242,7 +255,7 @@ async def spreadsheet_sse(token: str = Depends(oauth2_scheme)): ) -@app.get("/tell/spreadsheet") +@app.get("/sample/spreadsheet") async def spreadsheet(token: str = Depends(oauth2_scheme)) -> SampleShortInfoList: return get_spreadsheet(auth.parse_token(token)) @@ -280,10 +293,9 @@ async def raster(val: RasterGridRequest, token: str = Depends(oauth2_scheme)) -> @app.post("/scan/rotation") -async def rotation(val: RotationScanRequest, token: str = Depends(oauth2_scheme)): +async def rotation(val: RotationScanRequest, token: str = Depends(oauth2_scheme)) -> CompletedRotationScan: auth.check_jwt_rw(cfg, auth.parse_token(token)) - daq.measure_rotation(val) - return "OK" + return daq.measure_rotation(val) @app.post("/scan/auto") async def auto(s: SampleShortInfo, token: str = Depends(oauth2_scheme)): diff --git a/gui/src/aaregui/main_window.py b/gui/src/aaregui/main_window.py index 9910bb4e..c68e0cdf 100644 --- a/gui/src/aaregui/main_window.py +++ b/gui/src/aaregui/main_window.py @@ -21,6 +21,7 @@ from aaregui.panels.data_collection_settings import DataCollectionSettings from aaregui.panels.sample_queue_panel import SampleQueuePanel from aaregui.panels.tell_sample_panel import TellSamplePanel from aaregui.scan_logic.raster_grid_manager import RasterGridManager +from aaregui.scan_logic.rotation_scan_manager import RotationScanManager from aaregui.scan_logic.sample_mount_logic import SampleMountLogic from aaregui.threads.camera_thread import SampleCameraThread from aaregui.threads.daq_worker import DAQWorker @@ -73,6 +74,7 @@ class MainWindow(QMainWindow): ) self.raster = RasterGridManager(geom=geom) + self.rotation = RotationScanManager() collection_controls_scroll = NoWheelScrollArea(top_widget) @@ -130,8 +132,6 @@ class MainWindow(QMainWindow): self.daq = DAQWorker(base_url=self.__base_url, token=self.__token) self.daq.spreadsheet.connect(self.tell_samples.new_sample_list) - self.daq.raster_scan_completed.connect(self.raster.grid_scan_completed) - self.beamline.samcam.changed.connect(self.daq.samcam_settings) self.beamline.loopctr.background.clicked.connect(self.daq.alc_background) self.beamline.loopctr.find_tip.clicked.connect(self.daq.center_loop) @@ -179,7 +179,7 @@ class MainWindow(QMainWindow): self.daq.run_number_incremented.connect(self.data_collection.file_path_panel.increment_run_number) self.job_list_panel.auto_scan.connect(self.daq.automated_scan) - self.daq.automated_scan_done.connect(self.job_list_panel.automated_scan_done) + self.job_list_panel.unmount.connect(self.daq.unmount) self.tell_samples.mount.connect(self.daq.mount) @@ -208,6 +208,7 @@ class MainWindow(QMainWindow): self.status_bar.close_shutter.connect(self.daq.close_shutter) self.status_bar.open_shutter.connect(self.daq.open_shutter) self.raster.file_ready.connect(self.viewer.load_file) + self.rotation.file_ready.connect(self.viewer.load_file) self.raster.image_selected.connect(self.viewer.load_image) self.sample_logic.sample_changed.connect(self.data_collection.file_path_panel.update_sample) @@ -227,7 +228,10 @@ class MainWindow(QMainWindow): self.daq.update.connect(self.beamline.zoom_panel.update_daq_status) self.daq.update.connect(self.beamline.beam_mark.update_daq_status) self.daq.update.connect(self.sample_logic.update_daq_status) - self.daq.standard_scan_completed.connect(self.viewer.load_file) + + self.daq.standard_scan_completed.connect(self.rotation.scan_completed) + self.daq.automated_scan_done.connect(self.job_list_panel.automated_scan_done) + self.daq.raster_scan_completed.connect(self.raster.grid_scan_completed) def create_menu_bar(self): """Create a menu bar with File->Quit and Help->About.""" diff --git a/gui/src/aaregui/panels/manual_sample_panel.py b/gui/src/aaregui/panels/manual_sample_panel.py new file mode 100644 index 00000000..8ba9a9e0 --- /dev/null +++ b/gui/src/aaregui/panels/manual_sample_panel.py @@ -0,0 +1,41 @@ +from PySide6.QtCore import Signal, Slot +from PySide6.QtWidgets import QWidget, QGridLayout, QLabel, QTextEdit, QPushButton + +from aaredaqlib.models import SampleShortInfo +from aaregui.widgets.title_label import TitleLabel + + +class OmegaPanel(QWidget): + sample_manual = Signal(SampleShortInfo) + sample_clear = Signal() + + def __init__(self, parent=None): + super().__init__(parent) + self.__sample = SampleShortInfo( + db_id = -1, + puck_name="", + dewar_name="", + sample_name="Manual", + pin=0 + ) + + grid_layout = QGridLayout(self) + + grid_layout.addWidget(TitleLabel("Manual sample", self), 0, 0, 1, 2) + + grid_layout.addWidget(QLabel("Name"), 1, 0) + self._text_name = QTextEdit(self.__sample.sample_name) + grid_layout.addWidget(self._text_name, 1, 1) + self._text_name.textChanged.connect(self._name_changed) + self._add_button = QPushButton("Add") + grid_layout.addWidget(self._add_button, 2, 0, 1 ,2) + self._add_button.clicked.connect(self._add_clicked) + + @Slot(str) + def _name_vhanged(self, s: str): + self.__sample.sample_name = s + + @Slot() + def _add_clicked(self): + self.sample_manual.emit(self.__sample) + diff --git a/gui/src/aaregui/panels/rotation_data_collection.py b/gui/src/aaregui/panels/rotation_data_collection.py index 16c24042..f03cc32f 100644 --- a/gui/src/aaregui/panels/rotation_data_collection.py +++ b/gui/src/aaregui/panels/rotation_data_collection.py @@ -37,7 +37,7 @@ class RotationDataCollectionPanel(ScanSettingsPanel): self._layout.addWidget(QLabel("
Screening
", parent=self), 4, 0, 1,6) self._layout.addWidget(QLabel("Image angle", parent=self), 5, 0) - self.screening_image_angle = NumberLineEdit(0, 10.0, 0.5, decimals=3, parent=self) + self.screening_image_angle = NumberLineEdit(0, 90.0, 0.5, decimals=3, parent=self) self._layout.addWidget(self.screening_image_angle, 5, 1, 1, 3) self._layout.addWidget(QLabel("°", parent=self), 5, 4) diff --git a/gui/src/aaregui/scan_logic/raster_grid_manager.py b/gui/src/aaregui/scan_logic/raster_grid_manager.py index cce8a2d3..51b21390 100644 --- a/gui/src/aaregui/scan_logic/raster_grid_manager.py +++ b/gui/src/aaregui/scan_logic/raster_grid_manager.py @@ -10,6 +10,8 @@ from typing import List, Optional, Tuple from aaredaqlib.coordinate import Coordinate, SmargonCoordinate from aaredaqlib.models import DAQStatusModel from aaredaqlib.raster_grid import RasterGridRequest, CompletedRasterGrid +from aaredaqlib.sample_geometry import SampleGeometryModel + class RasterGridMetric(Enum): BKG = 0 @@ -74,11 +76,12 @@ class RasterGridManager(QObject): file_ready = Signal(str) image_selected = Signal(int) - def __init__(self, geom): + def __init__(self, geom: SampleGeometryModel): super().__init__() self.__sample_id = -1 + self.__beam_size_mm = geom.beam_size_mm self.__geom = geom self.__metric = RasterGridMetric.SPOTS @@ -120,7 +123,10 @@ class RasterGridManager(QObject): self.__sample_id = s.sample.db_id self.clear_completed_grids() self.clear_active_grid() - + if self.__beam_size_mm != s.geom.beam_size_mm: + self.__beam_size_mm = s.geom.beam_size_mm + self.__active_grid.grid_size_mm = Coordinate(x= 0.8 * self.__beam_size_mm.x, + y= 0.8 * self.__beam_size_mm.y) self.__geom = s.geom def resize_active_grid(self, end_point: QPointF): @@ -272,7 +278,7 @@ class RasterGridManager(QObject): grid = copy.deepcopy(self.__active_grid) self.grid_scan.emit(grid) - @Slot() + @Slot(CompletedRasterGrid) def grid_scan_completed(self, r: CompletedRasterGrid): self.__completed_grids.append(r) self.__active_grid.n_x = 0 diff --git a/gui/src/aaregui/scan_logic/rotation_scan_manager.py b/gui/src/aaregui/scan_logic/rotation_scan_manager.py index 08bf0583..9cba6262 100644 --- a/gui/src/aaregui/scan_logic/rotation_scan_manager.py +++ b/gui/src/aaregui/scan_logic/rotation_scan_manager.py @@ -1,6 +1,13 @@ -from PySide6.QtCore import QObject +from PySide6.QtCore import QObject, Signal, Slot + +from aaredaqlib.rotation_scan import CompletedRotationScan class RotationScanManager(QObject): + file_ready = Signal(str) + + @Slot(CompletedRotationScan) + def scan_completed(self, r: CompletedRotationScan): + if r.result.file_prefix is not None: + self.file_ready.emit(r.result.file_prefix) - pass diff --git a/gui/src/aaregui/threads/daq_worker.py b/gui/src/aaregui/threads/daq_worker.py index 78d1454a..fa3bfacc 100644 --- a/gui/src/aaregui/threads/daq_worker.py +++ b/gui/src/aaregui/threads/daq_worker.py @@ -9,7 +9,7 @@ from aaredaqlib.coordinate import SmargonCoordinate, Coordinate from aaredaqlib.models import DAQStatusModel, SampleShortInfoList, SampleShortInfo, SampleCameraSettings, \ AutofocusSettings from aaredaqlib.raster_grid import RasterGridRequest, CompletedRasterGrid -from aaredaqlib.rotation_scan import RotationScanRequest +from aaredaqlib.rotation_scan import RotationScanRequest, CompletedRotationScan SPREADHSEET_FREQUENCY = 25 # Every 5 seconds @@ -22,7 +22,7 @@ class DAQWorker(QObject): automated_scan_done = Signal(int, bool) # sample ID, success run_number_incremented = Signal() raster_scan_completed = Signal(CompletedRasterGrid) - standard_scan_completed = Signal(str) + standard_scan_completed = Signal(CompletedRotationScan) def __init__(self, base_url: str | None, token: str, parent=None): super().__init__(parent) @@ -195,18 +195,25 @@ class DAQWorker(QObject): def move_smargon(self, coord: SmargonCoordinate): self.generic_put("beamline/smargon", coord.model_dump_json()) + def handle_rotation_scan_response(self, reply: QNetworkReply): + try: + response_data = self.handle_response(reply) + parsed_response = CompletedRotationScan.model_validate_json(response_data) + self.standard_scan_completed.emit(parsed_response) + except Exception as e: + print(e) + self.http_error.emit(str(e)) + @Slot(RotationScanRequest) def standard_scan(self, r: RotationScanRequest): self.run_number_incremented.emit() - self.generic_post("scan/rotation", r.model_dump_json()) - request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/rotation/result")) + request = QNetworkRequest(QUrl(f"{self.__base_url}/scan/rotation")) 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"))) - if r.file_prefix is not None: - reply.finished.connect(lambda: self.standard_scan_completed.emit(r.file_prefix)) + reply.finished.connect(lambda: self.handle_rotation_scan_response(reply)) def handle_raster_scan_response(self, reply: QNetworkReply): try: @@ -252,10 +259,10 @@ class DAQWorker(QObject): @Slot() def load_spreadsheet(self): if self.__base_url is None: - print(f"GET /tell/spreadsheet") + print(f"GET /sample/spreadsheet") return - request = QNetworkRequest(QUrl(f"{self.__base_url}/tell/spreadsheet")) + request = QNetworkRequest(QUrl(f"{self.__base_url}/sample/spreadsheet")) request.setRawHeader(b"Authorization", f"Bearer {self.__token}".encode("utf-8")) reply = self.__net_manager.get(request) reply.finished.connect(lambda: self.handle_spreadsheet_response(reply)) @@ -318,12 +325,20 @@ class DAQWorker(QObject): @Slot() def unmount(self): - self.generic_post("tell/unmount") + self.generic_post("sample/unmount") @Slot(SampleShortInfo) def mount(self, s: SampleShortInfo): - self.generic_post(f"tell/mount?dbid={s.db_id}") + self.generic_post(f"sample/mount?dbid={s.db_id}") + + @Slot(SampleShortInfo) + def sample_manual(self, s: SampleShortInfo): + self.generic_post(f"sample/manual", s.model_dump_json()) + + @Slot() + def sample_clear(self): + self.generic_post("sample/clear") @Slot() def cancel(self): - self.generic_post("scan/cancel") \ No newline at end of file + self.generic_post("scan/cancel")