diff --git a/pyproject.toml b/pyproject.toml index a73470c8..6b2e43fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ requires-python = ">=3.11" dependencies = [ "uv", "gunicorn", - "aarecommon>=0.2.2", + "aarecommon>=0.5.1", "pydantic>=2.11", "numpy", "jfjoch_client==1.0.0rc146", diff --git a/src/aare/beamline_dispatch/beamline_dispatch.py b/src/aare/beamline_dispatch/beamline_dispatch.py index 80e1bb47..0a0281c9 100644 --- a/src/aare/beamline_dispatch/beamline_dispatch.py +++ b/src/aare/beamline_dispatch/beamline_dispatch.py @@ -13,7 +13,7 @@ def get_beamline_dispatch() -> BeamlineDispatch: case MXBeamline.X06DA: from .x06da import X06daDispatch - return X06daDispatch() + return X06daDispatch(MXBeamline.X06DA) case MXBeamline.X06SA: from .x06sa import X06saDispatch @@ -21,4 +21,4 @@ def get_beamline_dispatch() -> BeamlineDispatch: case MXBeamline.X10SA: from .x10sa import X10saDispatch - return X10saDispatch() + return X10saDispatch(MXBeamline.X10SA) diff --git a/src/aare/beamline_dispatch/default/beamline_dispatch.py b/src/aare/beamline_dispatch/default/beamline_dispatch.py index c432da92..03175a62 100644 --- a/src/aare/beamline_dispatch/default/beamline_dispatch.py +++ b/src/aare/beamline_dispatch/default/beamline_dispatch.py @@ -1,6 +1,13 @@ +import json import os +from abc import ABC +from importlib.resources import files -from aare.beamline_dispatch.protocols import AuthDispatch, BeamlineDispatch +from aarecommon.config.beamline import MXBeamline +from aarecommon.math.beam_center import BeamCenterFromDetectorStage +from aarecommon.models.beam_centre import BeamCentre + +from aare.beamline_dispatch.protocols import AuthDispatch, BeamlineDispatch, Geometry class DefaultAuthDispatch(AuthDispatch): @@ -12,13 +19,31 @@ class DefaultAuthDispatch(AuthDispatch): return key -class DefaultDispatch(BeamlineDispatch): +class DefaultGeometry(Geometry): + def __init__(self, beamline: MXBeamline) -> None: + super().__init__() + with open(str(files("aarecommon.config") / "beamline_configs" / "beam_centres.json")) as f: + measured = json.loads(f.read())[beamline.value.lower()] + self._model = BeamCentre.model_validate(measured) + self._beamline = beamline + + @property + def beam_centre_model(self) -> BeamCenterFromDetectorStage: + return self._model.model + + +class DefaultDispatch(BeamlineDispatch, ABC): """Default implementation for anything which can vary between beamlines and/or simulation. Should be safe and fail rather than assuming anything.""" - def __init__(self) -> None: + def __init__(self, beamline: MXBeamline) -> None: self._auth = DefaultAuthDispatch() + self._geo = DefaultGeometry(beamline=beamline) @property def auth(self): return self._auth + + @property + def geo(self): + return self._geo diff --git a/src/aare/beamline_dispatch/protocols.py b/src/aare/beamline_dispatch/protocols.py index d0675a7a..e6f2e340 100644 --- a/src/aare/beamline_dispatch/protocols.py +++ b/src/aare/beamline_dispatch/protocols.py @@ -1,6 +1,8 @@ from abc import ABC, abstractmethod from typing import Any +from aarecommon.math.beam_center import BeamCenterFromDetectorStage + class AuthDispatch(ABC): @abstractmethod @@ -28,6 +30,12 @@ class BecMacros(ABC): def mono_pitch_scan(plot=True): ... +class Geometry(ABC): + @property + @abstractmethod + def beam_centre_model(self) -> BeamCenterFromDetectorStage: ... + + class BeamlineDispatch(ABC): @property @abstractmethod @@ -35,3 +43,6 @@ class BeamlineDispatch(ABC): @property @abstractmethod def bec_macros(self) -> BecMacros: ... + @property + @abstractmethod + def geo(self) -> Geometry: ... diff --git a/src/aare/beamline_dispatch/x06da/beamline_dispatch.py b/src/aare/beamline_dispatch/x06da/beamline_dispatch.py index da0dbb87..f41434b4 100644 --- a/src/aare/beamline_dispatch/x06da/beamline_dispatch.py +++ b/src/aare/beamline_dispatch/x06da/beamline_dispatch.py @@ -1,5 +1,7 @@ from typing import Any +from aarecommon.config.beamline import MXBeamline + from aare.beamline_dispatch.default.beamline_dispatch import DefaultDispatch from aare.beamline_dispatch.protocols import BecMacros @@ -45,8 +47,8 @@ class X06daBecMacros(BecMacros): class X06daDispatch(DefaultDispatch): - def __init__(self) -> None: - super().__init__() + def __init__(self, beamline: MXBeamline) -> None: + super().__init__(beamline=beamline) self._bec_macros = X06daBecMacros() @property diff --git a/src/aare/beamline_dispatch/x10sa/beamline_dispatch.py b/src/aare/beamline_dispatch/x10sa/beamline_dispatch.py index 380513cb..c7cc2942 100644 --- a/src/aare/beamline_dispatch/x10sa/beamline_dispatch.py +++ b/src/aare/beamline_dispatch/x10sa/beamline_dispatch.py @@ -1,5 +1,7 @@ from typing import Any +from aarecommon.config.beamline import MXBeamline + from aare.beamline_dispatch.default.beamline_dispatch import DefaultDispatch from aare.beamline_dispatch.protocols import BecMacros @@ -39,8 +41,8 @@ class X10SaBecMacros(BecMacros): class X10saDispatch(DefaultDispatch): - def __init__(self) -> None: - super().__init__() + def __init__(self, beamline: MXBeamline) -> None: + super().__init__(beamline=beamline) self._bec_macros = X10SaBecMacros() @property diff --git a/src/aare/daq/config.py b/src/aare/daq/config.py index 421c1c91..24b44759 100644 --- a/src/aare/daq/config.py +++ b/src/aare/daq/config.py @@ -632,25 +632,6 @@ class BeamlineConfig: lens_factor = DEFAULT_LENS_MAGNIFICATION / lens_magnification return float(np.log(lens_factor / (b * target_pixel_in_mm)) / a) - @property - def beam_center(self) -> tuple[float, float]: - tmp_x = self._client.get(f"{self._bl}:beam_center_x") - tmp_y = self._client.get(f"{self._bl}:beam_center_y") - if tmp_x: - val_x = float(tmp_x) - else: - val_x = 0 - if tmp_y: - val_y = float(tmp_y) - else: - val_y = 0 - return val_x, val_y - - @beam_center.setter - def beam_center(self, data: tuple[float, float]): - self._client.set(f"{self._bl}:beam_center_x", data[0]) - self._client.set(f"{self._bl}:beam_center_y", data[1]) - @property def beam_size_mm(self) -> Coordinate: tmp_x = self._client.get(f"{self._bl}:beam_size_x") diff --git a/src/aare/daq/daq.py b/src/aare/daq/daq.py index 24301e54..faf7fcbc 100644 --- a/src/aare/daq/daq.py +++ b/src/aare/daq/daq.py @@ -67,6 +67,7 @@ from aarecommon.models.rotation_scan import CompletedRotationScan, RotationScanR from aarecommon.models.tell import TellPhaseEnum, TellStateModel from aareDB import SampleEventType +from aare.beamline_dispatch.protocols import BeamlineDispatch from aare.daq import workflows from aare.daq.aaredb import AareWrapper from aare.daq.config import ABR_POS_MOUNT, BeamlineConfig, BeamlineStateEnum @@ -270,10 +271,11 @@ class AareDAQ: AUTO_RASTER_MIN_CELL_SIZE_MM = 0.005 AUTO_RASTER_SKIP_IF_EXCEED_MAX_IMAGE_THRESHOLD = True - def __init__(self, cfg: BeamlineConfig, bl: MXBeamline): + def __init__(self, cfg: BeamlineConfig, bl: MXBeamline, dispatch: BeamlineDispatch): self.last_time = 0.0 + self._dispatch = dispatch self._cfg = cfg - self._devs = BeamlineDevices(bl) + self._devs = BeamlineDevices(bl, dispatch) self._mlbox = MlBox(bl) self._jfjoch = JFJochWrapper(bl) self._bl = bl.value.upper() @@ -2261,14 +2263,6 @@ class AareDAQ: ) return sample_geom - @property - def beam_center(self) -> tuple[float, float]: - return self._cfg.beam_center - - @beam_center.setter - def beam_center(self, val: tuple[float, float]): - self._cfg.beam_center = val - @property def beam_size_mm(self) -> Coordinate: return self._cfg.beam_size_mm @@ -3151,46 +3145,25 @@ class AareDAQ: @property def diffraction_geometry(self) -> DiffractionGeometry: - try: - metadata = self._cached_detector_metadata() - width = int(metadata.get("detector_width", 1)) - height = int(metadata.get("detector_height", 1)) - pixel_size_mm = float(metadata.get("pixel_size_mm", 0.15)) - detector_description = str(metadata.get("detector_description", "unavailable")) - detector_serial_number = str(metadata.get("detector_serial_number", "unavailable")) - energy = self._devs.energy_kev - dtz = self._devs.dtz - beam_center = self._cfg.beam_center - return DiffractionGeometry( - energy_keV=energy, - dtz_mm=dtz, - detector_size_pxl=(width, height), - pixel_size_mm=pixel_size_mm, - beam_center_pxl=beam_center, - detector_description=detector_description, - detector_serial_number=detector_serial_number, - poni_rot1_rad=-0.001396263, - poni_rot2_rad=-0.003839724, - ) - except Exception as e: - logger.warning( - f"Falling back to default diffraction geometry because cached detector metadata is unavailable: {e}", - exc_info=True, - ) - energy = self._devs.energy_kev - dtz = self._devs.dtz - beam_center = self._cfg.beam_center - return DiffractionGeometry( - energy_keV=energy, - dtz_mm=dtz, - detector_size_pxl=(1, 1), - pixel_size_mm=0.15, - beam_center_pxl=beam_center, - detector_description="unavailable", - detector_serial_number="unavailable", - poni_rot1_rad=-0.001396263, - poni_rot2_rad=-0.003839724, - ) + metadata = self._cached_detector_metadata() + width = int(metadata.get("detector_width", 1)) + height = int(metadata.get("detector_height", 1)) + pixel_size_mm = float(metadata.get("pixel_size_mm", 0.15)) + detector_description = str(metadata.get("detector_description", "unavailable")) + detector_serial_number = str(metadata.get("detector_serial_number", "unavailable")) + energy = self._devs.energy_kev + dtz = self._devs.dtz + return DiffractionGeometry( + energy_keV=energy, + dtz_mm=dtz, + detector_size_pxl=(width, height), + pixel_size_mm=pixel_size_mm, + beam_center_pxl=self._devs.detector_beam_centre_px, + detector_description=detector_description, + detector_serial_number=detector_serial_number, + poni_rot1_rad=-0.001396263, + poni_rot2_rad=-0.003839724, + ) @property def beamline_status(self) -> BeamlineStatus: diff --git a/src/aare/daq/devices.py b/src/aare/daq/devices.py index 16a46b99..aa0bd4bf 100644 --- a/src/aare/daq/devices.py +++ b/src/aare/daq/devices.py @@ -1,10 +1,6 @@ # Abstractions of devices for beamline import time -# Each "standard" device needs three elements: -# - property to read device value -# - setter with option to do sync/async move -# - property setter, which assumes that sync move is done (excl. zoom, which is async by default) from aarecommon.config.beamline import cfg_get from aarecommon.config.logger import setup_logger from aarecommon.config.logger_events import log_timing @@ -13,6 +9,11 @@ from aarecommon.models.beamline import MXBeamline from aarecommon.models.models import BeamlineStateEnum, SampleCameraSettings, StagePositionEnum from epics import PV +# Each "standard" device needs three elements: +# - property to read device value +# - setter with option to do sync/async move +# - property setter, which assumes that sync move is done (excl. zoom, which is async by default) +from aare.beamline_dispatch.protocols import BeamlineDispatch from aare.devices import aerotech, smargon from aare.devices.area_detector import AutoEnum, epicsAD from aare.devices.bec_worker import BECClientWorker @@ -27,7 +28,8 @@ logger = setup_logger("aareDAQ") class BeamlineDevices: - def __init__(self, beamline: MXBeamline): + def __init__(self, beamline: MXBeamline, dispatch: BeamlineDispatch): + self._dispatch = dispatch self._beamline = beamline BEAMLINE = beamline.value.upper() self.tell = make_tell_client(beamline) @@ -42,6 +44,7 @@ class BeamlineDevices: # faster to define the dtz object here than in functions and then use self._dtz = self.bec_worker.dev.det_z + self._dty = self.bec_worker.dev.det_y self.dtz_mod = cfg_get("daq.detector_distance_limit_modifier", 1.0) # TODO convert epics pvs to BEC self._sample_cam = epicsAD(f"{BEAMLINE}-ES-MS:") @@ -156,6 +159,13 @@ class BeamlineDevices: def lamp_light(self) -> float: return self._front_light.value + @property + def detector_beam_centre_px(self) -> tuple[float, float]: + beam_center_x, beam_center_y = self._dispatch.geo.beam_centre_model.predict( + self.dtz, self.dty + ) + return float(beam_center_x[0]), float(beam_center_y[0]) + @lamp_light.setter def lamp_light(self, v: float): self.set_front_light(v, wait=False) @@ -267,7 +277,11 @@ class BeamlineDevices: # Detector Z @property def dtz(self) -> float: - return self._dtz.read()["det_z"]["value"] + return self._dtz.user_setpoint.get() + + @property + def dty(self) -> float: + return self._dty.user_setpoint.get() @dtz.setter def dtz(self, value: float): diff --git a/src/aare/daq/server.py b/src/aare/daq/server.py index 8cf15fda..a33cf052 100644 --- a/src/aare/daq/server.py +++ b/src/aare/daq/server.py @@ -108,7 +108,7 @@ async def lifespan(application: FastAPI): # ── Core objects (Redis, EPICS PVs, BEC, TELL, JFJoch, etc.) ── bl = mx_beamline() cfg = BeamlineConfig(bl) - daq = AareDAQ(cfg, bl) + daq = AareDAQ(cfg, bl, bl_dispatch) cfg.state = daq.read_current_state_from_bec() try: @@ -932,25 +932,6 @@ async def clear_beam_mark(token: str = Depends(oauth2_scheme)): return "OK" -@app.post("/beamline/beam_center") -async def beam_center(x: float, y: float, token: str = Depends(oauth2_scheme)): - """ - Set the beam center position. Staff only. - - Args: - x: X coordinate in pixels. - y: Y coordinate in pixels. - token: OAuth2 access token. - - Returns: - "OK" on success. - """ - logger.debug(f"Beam Center {x}, {y}") - auth.check_jwt_staff(cfg, auth.parse_token(token)) - daq.beam_center = (x, y) - return "OK" - - @app.post("/beamline/beam_size_mm") async def beam_size_mm(x: float, y: float, token: str = Depends(oauth2_scheme)): """ diff --git a/src/aare/devices/bec_worker.py b/src/aare/devices/bec_worker.py index f5e51244..a06824fd 100644 --- a/src/aare/devices/bec_worker.py +++ b/src/aare/devices/bec_worker.py @@ -414,7 +414,7 @@ class BECClientWorker: logger.warning( f"Energy change may have failed, current energy: {self.check_current_energy()} eV" ) - if beamline is MXBeamline.X10SA: + if self.beamline is MXBeamline.X10SA: additonal_text = [ f"New dcm_bragg position: {self.dev.dcm_bragg.position:4g} mrad", f"New dcm_pitch position: {self.dev.dcm_pitch.position:4g} ", diff --git a/src/aare/gui/panels/beam_center_panel.py b/src/aare/gui/panels/beam_center_panel.py index 45236fbb..6aaf71ee 100644 --- a/src/aare/gui/panels/beam_center_panel.py +++ b/src/aare/gui/panels/beam_center_panel.py @@ -2,7 +2,6 @@ from aarecommon.models.models import DAQStatusModel from PySide6.QtCore import Signal, Slot from PySide6.QtWidgets import QGridLayout, QLabel, QWidget -from aare.gui.widgets.number_line_edit import NumberLineEdit from aare.gui.widgets.title_label import section_title @@ -16,23 +15,16 @@ class BeamCenterWidget(QWidget): grid_layout.addWidget(section_title("Beam center (detector)", self), 0, 0, 1, 5) - self.x = NumberLineEdit(-4000, 4000, 0, parent=self) - self.x.newValue.connect(self.beam_center_edited) - - self.y = NumberLineEdit(-4000, 4000, 0, parent=self) - self.y.newValue.connect(self.beam_center_edited) + self._x = QLabel("...") + self._y = QLabel("...") grid_layout.addWidget(QLabel("x:"), 1, 0) - grid_layout.addWidget(self.x, 1, 1) + grid_layout.addWidget(self._x, 1, 1) grid_layout.addWidget(QLabel("y:"), 1, 2) - grid_layout.addWidget(self.y, 1, 3) - grid_layout.addWidget(QLabel("pxl"), 1, 4) + grid_layout.addWidget(self._y, 1, 3) + grid_layout.addWidget(QLabel("px"), 1, 4) @Slot(DAQStatusModel) def update_daq_status(self, s: DAQStatusModel): - self.x.update_value(s.diffraction.beam_center_pxl[0]) - self.y.update_value(s.diffraction.beam_center_pxl[1]) - - @Slot(float) - def beam_center_edited(self, _: float): - self.beam_center.emit(self.x.value, self.y.value) + self._x.setText(f"{s.diffraction.beam_center_pxl[0]:.1f}") + self._y.setText(f"{s.diffraction.beam_center_pxl[1]:.1f}")