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] detector_description: str detector_serial_number: str poni_rot1_rad: float poni_rot2_rad: float @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)*0.5 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") x = self.wavelength_angstrom / (2 * exp_resolution_angstrom) if x >= 1.0 or x <= -1.0: return 0.0 theta = math.asin(x) return self.detector_radius_mm / math.tan(2*theta)