From 0af369fd124b2daf8f48b9d7029355ef7ad50e9a Mon Sep 17 00:00:00 2001 From: Filip Leonarski Date: Thu, 12 Jun 2025 16:49:27 +0200 Subject: [PATCH] DAQLib: Introduce a dedicated structure for Diffraction geometry --- common/src/aaredaqlib/diffraction.py | 47 ------------------- common/src/aaredaqlib/diffraction_geometry.py | 46 ++++++++++++++++++ common/src/aaredaqlib/models.py | 8 +--- daq/src/aaredaq/config.py | 28 +++++++---- daq/src/aaredaq/daq.py | 19 ++++++-- 5 files changed, 81 insertions(+), 67 deletions(-) delete mode 100644 common/src/aaredaqlib/diffraction.py create mode 100644 common/src/aaredaqlib/diffraction_geometry.py diff --git a/common/src/aaredaqlib/diffraction.py b/common/src/aaredaqlib/diffraction.py deleted file mode 100644 index e5e2c733..00000000 --- a/common/src/aaredaqlib/diffraction.py +++ /dev/null @@ -1,47 +0,0 @@ -import math - - -def cal_res_A(detector_distance_mm: float, - wavelength_angstrom: float, - radius_mm: float) -> float: - """ - Calculate maximum achievable resolution in Angstroms. - - Args: - detector_distance_mm: Distance from crystal to detector in mm - wavelength_angstrom: X-ray wavelength in Angstroms - radius_mm: Detector radius in mm - - Returns: - Resolution in Angstroms - """ - # Calculate theta using arctangent of opposite/adjacent - theta = math.atan(radius_mm / detector_distance_mm) / 2.0 - - # Using Bragg's law: n𝜆 = 2d*sin(θ) - # Solving for d: d = 𝜆/(2*sin(θ)) - resolution = wavelength_angstrom / (2 * math.sin(theta)) - return resolution - - -def calc_dtz_mm(desired_resolution_angstrom: float, - wavelength_angstrom: float, - radius_mm: float) -> float: - """ - Calculate required detector distance for desired resolution. - - Args: - desired_resolution_angstrom: Desired resolution in Angstroms - wavelength_angstrom: X-ray wavelength in Angstroms - radius_mm: Detector radius in mm - - Returns: - Required detector distance in mm - """ - # From Bragg's law: sin(θ) = 𝜆/(2d) - theta = math.asin(wavelength_angstrom / (2 * desired_resolution_angstrom)) - - # Using tangent: tan(2θ) = opposite/adjacent - # Therefore: distance = radius/tan(2θ) - distance = radius_mm / math.tan(2 * theta) - return distance diff --git a/common/src/aaredaqlib/diffraction_geometry.py b/common/src/aaredaqlib/diffraction_geometry.py new file mode 100644 index 00000000..c01b0fa9 --- /dev/null +++ b/common/src/aaredaqlib/diffraction_geometry.py @@ -0,0 +1,46 @@ +import math +from typing import Annotated, Tuple + +from pydantic import Field, BaseModel + +class DiffractionGeometry(BaseModel): + energy_keV: Annotated[float, Field(gt=1.0, lt=100.0)] + dtz_mm: Annotated[float, Field(gt=10.0, lt=5000.0)] + pixel_size_mm: Annotated[float, Field(ge=0.05, le=0.5)] + beam_center_pxl: Tuple[float, float] + detector_size_pxl: Tuple[int, int] + + + @property + def detector_max_radius_pxl(self): + x0 = self.detector_size_pxl[0] - self.beam_center_pxl[0] + x1 = self.beam_center_pxl[0] + y0 = self.detector_size_pxl[1] - self.beam_center_pxl[1] + y1 = self.beam_center_pxl[1] + return max(abs(x0), abs(x1), abs(y0), abs(y1)) + + @property + def detector_radius_mm(self): + return self.detector_max_radius_pxl * self.pixel_size_mm + + @property + def wavelength_angstrom(self): + return 12.398 / self.energy_keV + + @property + def max_resolution_angstrom(self): + return self.resolution_angstrom(self.dtz_mm) + + def resolution_angstrom(self, exp_dtz_mm: float) -> float: + if exp_dtz_mm <= 0: + raise ValueError("Detector distance must be positive") + + theta = math.atan(self.detector_radius_mm / exp_dtz_mm) / 2.0 + return self.wavelength_angstrom / (2 * math.sin(theta)) + + def calc_dtz_mm(self, exp_resolution_angstrom: float) -> float: + if exp_resolution_angstrom <= 0: + raise ValueError("Resolution must be positive") + + theta = math.asin(self.wavelength_angstrom / (2 * exp_resolution_angstrom)) + return self.detector_radius_mm / math.tan(2 * theta) \ No newline at end of file diff --git a/common/src/aaredaqlib/models.py b/common/src/aaredaqlib/models.py index 8ab2089e..d87949f6 100644 --- a/common/src/aaredaqlib/models.py +++ b/common/src/aaredaqlib/models.py @@ -4,6 +4,7 @@ from typing import Annotated, Literal, Tuple, List, Optional from pydantic import BaseModel, Field from aaredaqlib.coordinate import Coordinate +from aaredaqlib.diffraction_geometry import DiffractionGeometry from aaredaqlib.sample_geometry import SampleGeometryModel class TokenData(BaseModel): @@ -145,8 +146,6 @@ class AutofocusSettings(BaseModel): z_steps: int class BeamlineStatus(BaseModel): - energy_keV: float - dtz_mm: float ring_current_mA: float light: Annotated[float, Field(ge=0.0, le=100.0)] cryojet_K: float @@ -161,16 +160,13 @@ class SessionStatus(BaseModel): class DAQStatusModel(BaseModel): geom: SampleGeometryModel + diffraction: DiffractionGeometry bl: BeamlineStatus state: BeamlineStateEnum busy: bool sample: SampleShortInfo | None = None session: SessionStatus -class BeamCenterModel(BaseModel): - x: float | None = 0.0 - y: float | None = 0.0 - class BeamlineSettingsModel(BaseModel): # Float properties cryojet_park_position: float | None = 12.0 diff --git a/daq/src/aaredaq/config.py b/daq/src/aaredaq/config.py index 08e718e4..355b6b2d 100644 --- a/daq/src/aaredaq/config.py +++ b/daq/src/aaredaq/config.py @@ -1,6 +1,7 @@ import base64 import io import json +from typing import Tuple import numpy as np import redis @@ -13,7 +14,7 @@ from aaredaqlib.models import ( def_loop_centering_zoom, SampleShortInfo, SessionStatus, - BeamlineStateEnum, SessionsStateEnum, BeamCenterModel, + BeamlineStateEnum, SessionsStateEnum, ) from aaredaqlib.beamline import MXBeamline @@ -148,17 +149,24 @@ class BeamlineConfig: ) @property - def beam_center(self, dtz: float | None = None) -> BeamCenterModel: - tmp = self.__client.get(f"{self.__bl}:beam_center") - if tmp is None: - return BeamCenterModel() - data_dict = json.loads(tmp) - model = BeamCenterModel(**data_dict) - return model + 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: BeamCenterModel): - self.__client.set(f"{self.__bl}:beam_center", data.model_dump_json()) + 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]) def __get_settings(self) -> BeamlineSettingsModel: tmp = self.__client.get(f"{self.__bl}:settings") diff --git a/daq/src/aaredaq/daq.py b/daq/src/aaredaq/daq.py index 2eba20d6..30802b51 100644 --- a/daq/src/aaredaq/daq.py +++ b/daq/src/aaredaq/daq.py @@ -8,6 +8,7 @@ import redis from aaredaq.autofocus import calculate_focus_measure from aaredaqlib.coordinate import Coordinate, SmargonCoordinate +from aaredaqlib.diffraction_geometry import DiffractionGeometry from aaredaqlib.models import ( SampleShortInfo, PuckLoadedInfo, @@ -398,7 +399,8 @@ class AareDAQ: y=self.__devs.gmy.value - abr_pos.y, z=self.__devs.gmz.value - abr_pos.z, ), - aerotech_meas=self.__devs.abr_pos + aerotech_meas=self.__devs.abr_pos, + detector_radius_mm = 115.0 # PILATUS4 2M / JUNGFRAU 9M ) def get_beam_center(self): @@ -711,10 +713,18 @@ class AareDAQ: self.__devs.shutter = False @property - def beamline_status(self) -> BeamlineStatus: - return BeamlineStatus( + def diffraction_geometry(self) -> DiffractionGeometry: + return DiffractionGeometry( energy_keV=self.__devs.energy_kev, dtz_mm=self.__devs.dtz.value, + detector_size_pxl=(1553,1630), + pixel_size_mm=0.150, #PILATUS 4 + beam_center_pxl=self.__cfg.beam_center + ) + + @property + def beamline_status(self) -> BeamlineStatus: + return BeamlineStatus( ring_current_mA=self.__devs.ring_current, light=self.light, cryojet_K=self.__devs.cryojet_temp, @@ -732,6 +742,7 @@ class AareDAQ: geom=self.sample_geometry, bl=self.beamline_status, sample=self.sample, - session=SessionStatus() + session=SessionStatus(), + diffraction=self.diffraction_geometry )