Files
AareDAQ/daq/src/aaredaq/daq.py
T

1615 lines
68 KiB
Python

import copy
import json
import secrets
import time
from datetime import datetime
from math import ceil
from typing import List, Tuple, Optional
import cv2
import numpy as np
import redis
import aaredaqlib.face_detection as fd
from aaredaq import workflows
from aaredaq.aaredb import AareWrapper
from aaredaq.autofocus import calculate_focus_measure
from aaredaq.config import BeamlineConfig, ABR_POS_MOUNT
from aaredaq.config import BeamlineStateEnum
from aaredaq.devices import BeamlineDevices
from aaredaq.mlbox import MlBox
from aaredaqlib.beamline import MXBeamline
from aaredaqlib.coordinate import Coordinate, SmargonCoordinate
from aaredaqlib.diffraction_geometry import DiffractionGeometry
from aaredaqlib.find_xtal import raster_centre_of_mass, create_quality_filtered_array, identify_crystal_raster, \
get_result_list_from_com, get_best_b_factor, get_best_res, get_xtal_size
from aaredaqlib.logger_config import setup_logger
from aaredaqlib.models import (
SampleShortInfo,
PuckLoadedInfo,
SampleShortInfoList,
DAQStatusModel, BeamlineStatus, SessionStatus, SampleCameraSettings, AutofocusSettings, ZoomModeEnum, CrystalSize,
SimpleScanParameters, MLBoxModel, FluorescenceSpectrumParameterModel,
FluorescenceSpectrumOutputModel)
from aaredaqlib.raster_grid import RasterGridRequest, CompletedRasterGrid, CompletedRasterGridElem
from aaredaqlib.rotation_scan import RotationScanRequest, CompletedRotationScan
from aaredaqlib.sample_geometry import SampleGeometryModel
from mxlibs3.jfjoch import JFJochWrapper
logger = setup_logger("aareDAQ", "/tmp/mxlogs")
class TransformationInvalidException(Exception):
def __init__(self, message="Transformation is not implemented"):
super().__init__(message)
self.message = message
logger.error(f"{message}", extra={"exception:" : Exception})
def __str__(self):
return self.message
class LoopCenteringFailed(Exception):
#logger.error(f"Loop centering failed", extra={"exception:" : Exception})
pass
class AareDAQ:
def __init__(self, cfg: BeamlineConfig, bl: MXBeamline):
self.last_time = 0.0
self.__cfg = cfg
self.__devs = BeamlineDevices(bl)
self.__mlbox = MlBox()
self.__jfjoch = JFJochWrapper(bl)
self.__bl = bl.value.upper()
self.__aare = AareWrapper(bl)
self.__saved_box = None
@property
def state(self) -> BeamlineStateEnum:
return self.__cfg.state
@property
def busy(self) -> bool:
return self.__cfg.state_busy
@state.setter
def state(self, target: BeamlineStateEnum):
if target == BeamlineStateEnum.Moving:
logger.error(f"Cannot explicitly move to busy state", extra={"target":target, "state":self.__cfg.state})
raise Exception("Cannot explicitly move to busy state")
start = time.perf_counter()
self.__cfg.try_set_busy(timeout=300)
self.__set_state(target)
self.__cfg.state_busy = False
end = time.perf_counter()
self.last_time = end - start
def spreadsheet_params(self) -> tuple[Optional[SimpleScanParameters], str|None]:
file_prefix = None
if self.status.sample is None:
return None, file_prefix
logger.debug(f"generate params for sample: {self.status.sample}")
aaredb_params = self.status.sample.aaredb_params if hasattr(self.status.sample, "aaredb_params") else None
if aaredb_params is None:
return None, file_prefix
#if aaredb_params.directory:
# file_prefix = aaredb_params.directory
if (
getattr(aaredb_params, 'exposure', None) is None
and getattr(aaredb_params, 'transmission', None) is None
and getattr(aaredb_params, 'oscillation', None) is None
and getattr(aaredb_params, 'totalrange', None) is None
and getattr(aaredb_params, 'targetresolution', None) is None
):
return None, file_prefix
params = SimpleScanParameters()
if (exp := getattr(aaredb_params, 'exposure', None)) is not None:
params.exp_time_s = exp
if (trans := getattr(aaredb_params, 'transmission', None)) is not None:
logger.debug(f"transmission: {trans}")
params.transmission = trans / 100.0 if trans > 1.0 else trans
if (res := getattr(aaredb_params, 'targetresolution', None)) is not None:
logger.debug(f"resolution: {res}")
logger.debug(f"requested dtz: {self.diffraction_geometry.calc_dtz_mm(res)} ")
new_res = 1 / ((1 / res) + 0.1)
corrected_dtz = self.diffraction_geometry.calc_dtz_mm(new_res)
logger.debug(f"corrected dtz: {corrected_dtz}")
if corrected_dtz < 108:
corrected_dtz = 108
params.dtz = round(corrected_dtz)
osc = getattr(aaredb_params, 'oscillation', None)
total = getattr(aaredb_params, 'totalrange', None)
if osc is not None:
osc = abs(osc)
if osc > 0:
params.incr_omega_deg = osc
if total is not None and abs(total) > 0:
params.steps = round(abs(total) / osc)
else:
params.steps = round(360.0 / osc)
elif total is not None and abs(total) > 0:
default_osc = SimpleScanParameters().incr_omega_deg
params.incr_omega_deg = default_osc
params.steps = round(abs(total) / default_osc)
#if file_prefix is not None:
# params.file_prefix = file_prefix
return params, file_prefix
@property
def omega(self) -> float:
return self.__devs.aerotech.omega
@omega.setter
def omega(self, val: float):
self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
self.__saved_box = None
if -720 < val < 720:
self.__devs.aerotech.move(val, wait=True, speed=180.0, direct=True)
self.__cfg.state_busy = False
else:
self.__cfg.state_busy = False
logger.error("Omega has to be between -720 and 720 degrees")
raise ValueError("Omega has to be between -720 and 720 degrees (for now)")
@property
def zoom(self) -> float:
return self.__devs.zoom
@zoom.setter
def zoom(self, val: float):
self.__saved_box = None
self.__devs.zoom = val
print(f"{self.__cfg.state}")
if self.__cfg.state == BeamlineStateEnum.BeamLocation:
self.__cfg.zoom_mode = ZoomModeEnum.BeamLocation
else:
self.__cfg.zoom_mode = ZoomModeEnum.User
zoom_settings = self.__cfg.zoom_settings.get_camera_settings(val)
print(f"Setting zoom {val} {zoom_settings}")
self.samcam_settings = zoom_settings
#elf.__cfg.state_busy = False
@property
def light(self) -> float:
val = self.__devs.lamp_light
if val <= 1.0:
return 0
elif val >= 2.5:
return 100.0
else:
return (val - 1.0) / 1.5 * 100.0
@light.setter
def light(self, f: float):
conv = (f / 100.0 * 1.5) + 1.0
print(f"Light {f} -> {conv}")
self.__devs.lamp_light = conv
@property
def sample(self) -> SampleShortInfo | None:
if self.__cfg.current_sample is not None and self.__cfg.current_sample.location is None:
return self.__cfg.current_sample
if self.__devs.tell.get_mounted_sample() is None:
return None
return self.__cfg.current_sample
@property
def samcam_settings(self) -> SampleCameraSettings:
return self.__devs.samcam_settings
@samcam_settings.setter
def samcam_settings(self, s: SampleCameraSettings):
self.__devs.samcam_settings = s
def autofocus(self, f: AutofocusSettings):
pos = self.__cfg.abr_meas_pos
z_min = pos.z - f.z_range_um / 2.0
z_max = pos.z + f.z_range_um / 2.0
measures = []
for i in range(f.z_steps):
pos.z = z_min + i * (z_max - z_min) / f.z_steps
self.__devs.abr_pos = pos
self.__devs.sample_cam.get_single_image()
image = self.__devs.sample_cam.get_image(gray=True)
val = calculate_focus_measure(image, f.center_x_pxl, f.center_y_pxl, f.radius_pxl)
measures.append((pos.z, val))
best_pos, _ = max(measures, key=lambda x: x[1])
pos.z = best_pos
self.__devs.abr_pos = pos
self.__cfg.abr_meas_pos = pos
self.__devs.sample_cam.collect_auto()
def tweak_abr_meas_pos(self, c: Coordinate):
self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
try:
new_meas_pos = self.__cfg.abr_meas_pos + c
self.__cfg.abr_meas_pos = new_meas_pos
self.__devs.abr_pos = new_meas_pos
self.__saved_box = None
self.__cfg.state_busy = False
except:
self.__cfg.state_busy = False
raise
def save_abr_meas_pos(self):
self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
try:
self.__cfg.abr_meas_pos = self.__devs.abr_pos
self.__cfg.state_busy = False
except:
self.__cfg.state_busy = False
raise
def goto_abr_meas_pos(self):
self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
try:
self.__devs.abr_pos = self.__cfg.abr_meas_pos
self.__cfg.state_busy = False
except:
self.__cfg.state_busy = False
raise
def create_sample(self, target: SampleShortInfo):
self.__cfg.try_set_busy(timeout=360)
try:
curr_sample = self.__cfg.current_sample
if curr_sample is not None and curr_sample.location is not None:
raise Exception("Sample from TELL is loaded")
self.__aare.create_manual_sample(target)
self.__cfg.current_sample = target
self.__cfg.state_busy = False
except:
self.__cfg.state_busy = False
raise
def __mount(self, target: SampleShortInfo | None):
self.__set_state(BeamlineStateEnum.RobotSampleExchange)
self.__saved_box = None
logger.info("Moving smargon to home")
self.__devs.smargon.move_home(wait=True)
logger.info("moving aerotech to mount position")
self.__devs.abr_pos = ABR_POS_MOUNT
time.sleep(0.1)
logger.info("checking beamstop")
if self.__devs.bsz.value < 24.0:
raise Exception("Beamstop Z below 24.0 mm - potentially unsafe with mounting")
logger.info(f"Checking magnet postion sensor positon: {self.__devs.magnet_position_sensor_readout.value}")
logger.info(f"Checking smargon position: {self.__devs.smargon.readback}")
logger.info(f"Checking abr position: {self.__devs.abr_pos}")
if self.__devs.magnet_position_sensor.value != 0:
time.sleep(1)
logger.warning("!!!!!!!!!!!!!!!!!!!!!MAGNET CONTROLLER BROKE AGAIN!!!!!!!!!!!!!!!!!!")
logger.debug(f"Checking magnet position sensor positon: {self.__devs.magnet_position_sensor_readout.value}")
logger.debug(f"Checking smargon position: {self.__devs.smargon.readback}")
logger.debug(f"Checking abr position: {self.__devs.abr_pos}")
if self.__devs.magnet_position_sensor.value != 0:
start = time.time()
end = start + 360
while self.__devs.magnet_position_sensor.value != 0:
time.sleep(1)
logger.debug(f"Checking magnet position sensor positon: {self.__devs.magnet_position_sensor_readout.value}")
logger.debug(f"Checking smargon position: {self.__devs.smargon.readback}")
logger.debug(f"Checking abr position: {self.__devs.abr_pos}")
if time.time() > end:
raise Exception(f"Magnet position sensor is not in position: {self.__devs.magnet_position_sensor_readout.value}")
#Reenable TELL after doors locked
logger.info("enable tell motion after doors locked")
self.__devs.tell.check_enable_motion()
self.__devs.tell.wait_mount_complete()
logger.debug("Moving tell to mount position")
self.__devs.tell.set_in_mount_position(True)
curr_sample = self.__cfg.current_sample
logger.info("Waiting for tell to be ready")
self.__devs.tell.wait_ready()
if curr_sample is not None and curr_sample.location is None:
logger.info(f"Unmounting current sample")
# 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:
logger.info(f"Moving tell to unmount with no target")
self.__devs.tell.unmount(wait=True, timeout=360)
self.__aare.sample_unmounted(curr_sample)
self.__cfg.current_sample = None
else:
# reset zoom
self.zoom = 1
# mount sample
self.__aare.sample_unmounted(curr_sample)
logger.info(f"Moving tell to mount")
value = self.__devs.tell.mount(
address=target.tell_address(),
force=True,
auto_unmount=True,
read_dm=False,
wait=True,
timeout=360,
)
logger.debug(f"Post tell mount, pre db input")
if value is not None:
if value == "No Pin in Gripper":
logger.error("No sample was detected in gripper")
self.__devs.tell.dry(wait=True)
self.__aare.sample_failed(target, failed_comment = "No sample was detected in gripper")
raise Exception("No sample was detected in gripper, drying gripper")
elif value == "dry":
logger.info("Robot is drying")
else:
logger.info(f"Event detected from robot: {value}")
mounted = self.__devs.tell.get_mounted_sample()
logger.info(f"response from tell {mounted}")
if not mounted:
logger.error(f"Failed to mount target: {target.db_id} {target.location} {target.pin}")
self.__aare.sample_failed(target, failed_comment = "No sample was mounted")
raise Exception("No sample was mounted")
if target is not None:
self.__aare.sample_mounted(target)
logger.info(f"Target mounted: {target.db_id} {target.location} {target.pin}")
self.save_screenshot_db(target.db_id, f"sample_mounted")
else:
logger.info(f"Target is None: {target}")
self.__cfg.current_sample = target
# TODO: Need to know if dry needs to happen
@sample.setter
def sample(self, target: SampleShortInfo | None):
# This will set internally state to SampleExchange, but will return to SampleAlignment
# before exiting
self.__cfg.try_set_busy(timeout=360)
try:
curr_sample = self.__cfg.current_sample
curr_sample_is_manual = False
logger.info(f"current sample: {curr_sample}")
logger.info(f"new target is: {target}")
self.__cfg.crystal_size = CrystalSize(x=0, y=0, z=0)
self.__cfg.last_best_b_factor = None
self.__cfg.last_best_res = None
self.__cfg.xrf = None
if curr_sample is not None and curr_sample.location is None:
self.__cfg.current_sample = None
curr_sample_is_manual = True
if target is not None or not curr_sample_is_manual:
self.__mount(target)
if target is not None and target.db_id is not None:
#self.__aare.sample_mounted(self.__cfg.current_sample)
logger.info(f"post_mount_{target.db_id}_{self.__devs.zoom}")
self.save_screenshot_db(target.db_id , f"post_mount_{target.db_id }_{self.__devs.zoom}")
except Exception as e:
self.__cfg.state_busy = False
# If unmount succeeded, but mount failed
mounted_sample = self.__devs.tell.get_mounted_sample()
#TODO check the following error logic
# if target is not None:
# print(f"TELL: Error mounting, TELL believes current sample is {mounted_sample}, while expected {target.location}")
if mounted_sample is None:
logger.error("TELL: After mounting there is no sample on gonio according to TELL")
self.__cfg.current_sample = None
if target is not None:
raise e
elif target is not None and (target.location is not None
and mounted_sample.puck.pos == target.location.pos
and mounted_sample.puck.segment == target.location.segment
and mounted_sample.pin == target.pin):
logger.info("TELL: Actually mounting was correct according to TELL, so setting current sample to target")
self.__cfg.current_sample = target
else:
raise e
self.__set_state(BeamlineStateEnum.SampleAlignment)
self.__cfg.state_busy = False
#if target is not None and target.location is not None:
# self.__loop_center_sequence(target.db_id)
@property
def camera_image(self) -> np.ndarray | None:
image = self.__devs.sample_cam.get_image(gray=False)
return image
@property
def camera_image_gray(self) -> np.ndarray | None:
image = self.__devs.sample_cam.get_image(gray=True)
return image
def list_loaded_pucks(self) -> List[PuckLoadedInfo]:
return self.__devs.tell.get_detected_pucks()
def __auto_center(self, grid: RasterGridRequest) -> CompletedRasterGrid | None:
sample = self.sample
if sample is None:
raise Exception("Sample must be mounted to auto center")
old_prefix = grid.file_prefix
geom = self.sample_geometry
r = self.__ml_bounding_box(sample.db_id, f"ml_{geom.omega_deg:.2f}deg")
if r is None:
self.__devs.aerotech.move(geom.omega_deg + 90.0, wait=True)
time.sleep(0.2)
r = self.__ml_bounding_box(sample.db_id, f"ml_{geom.omega_deg + 90.0:.2f}deg")
if r is not None:
geom = self.sample_geometry
grid.smargon_top_left = r.smargon_top_left
grid.grid_size_mm = r.grid_size_mm
grid.n_x = r.n_x
grid.n_y = r.n_y
grid.file_prefix = f"{old_prefix}_{grid.omega_deg}deg"
grid.omega_deg = geom.omega_deg
res1 = self.__raster(grid)
self.__devs.reflector_up = True
time.sleep(0.1)
self.save_screenshot_db(sample.db_id, f"post_raster_{grid.omega_deg}deg")
time.sleep(0.1)
self.__devs.reflector_up = False
grid.omega_deg += 90
self.__devs.aerotech.move(grid.omega_deg, wait=True)
grid.n_x = 1
#TODO generate y scan rather than had code for 1x50
grid.n_y = 50
grid.file_prefix = f"{old_prefix}_{grid.omega_deg}deg"
grid.grid_size_mm = Coordinate(x=geom.beam_size_mm.x, y=geom.beam_size_mm.y * 0.25)
offset = Coordinate(x=-grid.grid_size_mm.x/2.0, y=-(grid.n_y + 0.5) * grid.grid_size_mm.y / 2.0)
geom = self.sample_geometry
logger.debug(f"set offset y scan {offset}")
# self.__devs.smargon.target = geom.translate_smargon(offset)
grid.smargon_top_left = geom.translate_smargon(offset)
res2 = self.__raster(grid)
self.__devs.reflector_up = True
time.sleep(0.1)
self.save_screenshot_db(sample.db_id, f"post_raster_{grid.omega_deg}deg")
time.sleep(0.1)
self.__devs.reflector_up = False
return CompletedRasterGrid(r=[res1, res2])
else:
return None
def __raster(self, r: RasterGridRequest) -> CompletedRasterGridElem:
max_time = r.exp_time_s * r.n_y * r.n_x + 60
if r.dtz is not None:
self.__cfg.dtz = r.dtz
self.__set_state(BeamlineStateEnum.DataCollection)
save_smargon_position = self.__devs.smargon.readback
if r.smargon_top_left is not None:
delta_mm = self.sample_geometry.smargon_nudge(Coordinate(x=r.grid_size_mm.x / 2, y=r.grid_size_mm.y / 2))
self.__devs.smargon.target = SmargonCoordinate(sh_mm=r.smargon_top_left.sh_mm + delta_mm,
phi_deg=r.smargon_top_left.phi_deg,
chi_deg=r.smargon_top_left.chi_deg)
if r.transmission is not None:
self.__devs.transmission.set(r.transmission, wait=False)
self.__devs.aerotech.move(r.omega_deg, wait=True, speed=360.0)
self.__devs.transmission.wait()
self.__devs.smargon.wait()
status = self.status
logger.info(f"raster status: {status}")
logger.info(f"raster grid request: {r}")
self.__jfjoch.measure_raster(r, status)
self.__aare.create_gridscan_run(self.sample, r, status)
if self.sample is not None and self.sample.db_id is not None:
self.save_screenshot_db(self.sample.db_id, "before_raster")
if r.n_x == 1:
self.__devs.aerotech.measure_vertical_line(r.exp_time_s, r.grid_size_mm.y, r.n_y)
else:
self.__devs.aerotech.measure_raster_simple(
r.exp_time_s, r.grid_size_mm.x, r.grid_size_mm.y, r.n_x, r.n_y
)
self.__devs.aerotech.wait_scan_done(max_time)
self.__devs.aerotech.reset()
self.__devs.abr_pos = self.__cfg.abr_meas_pos
result = self.__jfjoch.wait_till_done(60)
images = result.images
result_array = create_quality_filtered_array(images, 'spots_low_res', min_spots=None,
min_efficiency=1.0, min_background=None, min_low_res_spots=None)
self.__cfg.crystal_size = get_xtal_size(self.__cfg.crystal_size, result_array, r)
com = raster_centre_of_mass(result_array, r)
if com is None:
logger.debug(f"using old method as COM is disabled")
com = identify_crystal_raster(result, r)
if com:
com_mm = com.get_com_mm(r)
grid_mm_x = com_mm.x
grid_mm_y = com_mm.y
else:
grid_mm_x = None
grid_mm_y = None
if grid_mm_x or grid_mm_y:
new_delta_mm = self.sample_geometry.smargon_nudge(Coordinate(x=grid_mm_x, y=grid_mm_y))
if r.n_x != 1:
result_list = get_result_list_from_com(images, com)
self.__cfg.last_best_b_factor = get_best_b_factor(result_list)
self.__cfg.last_best_res = get_best_res(result_list)
logger.debug(f"b_factor: {self.__cfg.last_best_b_factor}, best_res: {self.__cfg.last_best_res}")
else:
new_delta_mm = None
if new_delta_mm is not None:
if r.smargon_top_left:
new_target = r.smargon_top_left
else:
new_target = save_smargon_position
logger.debug(f'moving SMARGON to target new delta mm {new_target.sh_mm + new_delta_mm} mm')
self.__devs.smargon.target = SmargonCoordinate(sh_mm=new_target.sh_mm + new_delta_mm,
phi_deg=new_target.phi_deg,
chi_deg=new_target.chi_deg)
else:
logger.error("Auto finding optimal image failed due to no images found. Using previous position.")
self.__devs.smargon.target = save_smargon_position
if self.sample is not None and self.sample.db_id is not None and result is not None:
try:
self.__aare.ingest_gridscan(sample = self.sample, raster_result =result,
raster_request = r, geom = self.sample_geometry,
com = com)
except Exception as e:
logger.error(f"Exception ingesting grid scan: {e}")
return CompletedRasterGridElem(request=copy.deepcopy(r), result=result)
def measure_raster(self, r: RasterGridRequest, auto: bool) -> CompletedRasterGrid:
self.__cfg.try_set_busy(timeout=ceil(360))
try:
if auto:
result = self.__auto_center(r)
else:
raster_result = self.__raster(r)
result = CompletedRasterGrid(r=[raster_result])
self.__devs.reflector_up = True
time.sleep(0.1)
if self.sample is not None and self.sample.db_id is not None:
self.save_screenshot_db(self.sample.db_id, f"post_raster_{r.omega_deg}deg")
time.sleep(0.1)
self.__set_state(BeamlineStateEnum.SampleAlignment)
self.__cfg.state_busy = False
return result
except Exception as e:
try:
self.__aare.axc_failed(self.sample)
except Exception as axc_e:
logger.error(f"Exception while reporting AXC failure: {axc_e}")
self.__set_state(BeamlineStateEnum.SampleAlignment)
self.__cfg.state_busy = False
raise e
def __rotation(self, request: RotationScanRequest) -> CompletedRotationScan:
if request.dtz is not None:
logger.info(f'requesting dtz to move to {request.dtz}')
self.__cfg.dtz = request.dtz
if self.sample is not None and self.sample.db_id is not None:
self.save_screenshot_db(self.sample.db_id, f"pre_rotation{self.sample.db_id}_{self.__devs.zoom}")
self.__set_state(BeamlineStateEnum.DataCollection)
if request.transmission is not None:
logger.info(f'requesting transmission to move to {request.transmission}')
self.__devs.transmission.set(request.transmission, wait=False)
if request.start is not None:
self.__devs.smargon.target = request.start
self.__devs.transmission.wait()
if request.start is not None:
self.__devs.smargon.wait()
status = self.status
self.__jfjoch.measure_rotation(request, status, self.__cfg.xrf)
self.__aare.create_rotation_run(self.sample, request, status)
if self.sample is not None and self.sample.db_id is not None:
self.save_screenshot_db(self.sample.db_id, "before_dc")
if request.screening:
self.__devs.aerotech.measure_screening(request.start_omega_deg,
request.wedge_omega_deg,
request.exp_time_s,
request.incr_omega_deg,
request.steps)
self.__devs.aerotech.wait_scan_done(request.exp_time_s * request.steps + 60)
else:
total_omega = request.incr_omega_deg * request.steps
total_time = request.exp_time_s * request.steps
self.__devs.aerotech.measure_standard(
request.start_omega_deg, total_omega, total_time
)
if request.start is not None and request.end is not None:
smargon_time_step = request.time_sec / float(request.steps)
pos_step = (request.end.sh_mm - request.start.sh_mm) * (1.0 / float(request.steps))
for i in range(request.steps):
self.__devs.smargon.target = SmargonCoordinate(
sh_mm=request.start.sh_mm + pos_step * i
)
time.sleep(smargon_time_step)
self.__devs.aerotech.wait_scan_done(total_time + 60)
self.__devs.aerotech.reset()
result = self.__jfjoch.wait_till_done(60)
self.__aare.sample_collected(self.sample)
if self.sample is not None and self.sample.db_id is not None:
self.save_screenshot_db(self.sample.db_id, "after_dc")
return CompletedRotationScan(request=copy.deepcopy(request), result=result)
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:
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
raise e
def get_background(self):
#if self.sample is not None:
# raise Exception("Background cannot be measured when sample is mounted")
self.__cfg.try_set_busy(timeout=360)
self.__set_state(BeamlineStateEnum.SampleAlignment)
try:
self.__devs.lamp_light = 2.5
self.__cfg.zoom_mode = ZoomModeEnum.LoopCenter
zoom_settings = self.__cfg.zoom_settings.z
for zoom_value in zoom_settings:
exposure = zoom_settings[zoom_value].exposure
gain = zoom_settings[zoom_value].gain
self.__devs.samcam_settings = SampleCameraSettings(exposure=exposure, gain=gain)
self.__devs.zoom_sync(zoom_value)
time.sleep(5.0) # Wait for settings to stabilize
image = self.__devs.sample_cam.get_image(gray=False)
self.__cfg.put_alc_bkg(zoom_value, exposure, gain, image)
bgr_array = cv2.cvtColor(image, cv2.COLOR_RGB2BGR)
cv2.imwrite(f"bkg{zoom_value:.0f}_{exposure*1000:.0f}_{gain:.0f}.jpg", bgr_array)
self.__cfg.state_busy = False
self.__devs.sample_cam.set_auto()
except Exception as e:
self.__cfg.state_busy = False
raise e
@property
def dtz(self) -> float:
tmp = self.__cfg.dtz
if tmp is None:
return 150.0
else:
return tmp
@dtz.setter
def dtz(self, val: float):
self.__cfg.try_set_busy(timeout=360)
state = self.__cfg.state
if val < self.__devs.dtz_low or val > self.__devs.dtz_high:
self.__cfg.state_busy = False
raise RuntimeError(f"dtz={val} outside limits {self.__devs.dtz_low} to {self.__devs.dtz_high}")
if state == BeamlineStateEnum.DataCollection:
self.__cfg.state_busy = False
raise RuntimeError("Cannot set dtz during data collection")
elif state == BeamlineStateEnum.SampleAlignment:
self.__devs.dtz.move(val, wait=False)
self.__cfg.dtz = val
self.__cfg.state_busy = False
@property
def smargon(self) -> SmargonCoordinate:
return self.__devs.smargon.readback
@smargon.setter
def smargon(self, sc: SmargonCoordinate):
self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
try:
self.__saved_box = None
self.__devs.smargon.target = sc
self.__devs.smargon.wait()
self.__cfg.state_busy = False
except Exception as e:
self.__cfg.state_busy = False
raise e
pass
def mark_beam(self, x_pxl: float, y_pxl: float):
self.__cfg.set_busy(BeamlineStateEnum.BeamLocation)
try:
self.__cfg.mark_beam(x_pxl, y_pxl, self.__devs.zoom)
self.__cfg.state_busy = False
except Exception as e:
self.__cfg.state_busy = False
raise e
def clear_mark_beam(self):
self.__cfg.set_busy(BeamlineStateEnum.BeamLocation)
self.__cfg.clear_mark_beam()
self.__cfg.state_busy = False
@property
def sample_geometry(self) -> SampleGeometryModel:
zoom = self.__devs.zoom
abr_pos = self.__cfg.abr_meas_pos
try:
aerotech_coord = Coordinate(
x=self.__devs.gmx.value - abr_pos.x,
y=self.__devs.gmy.value - abr_pos.y,
z=self.__devs.gmz.value - abr_pos.z,
)
sample_geom = SampleGeometryModel(
beam_location_pxl=self.__cfg.beam_mark_coeff.apply(zoom),
pixel_in_mm=self.__cfg.pixel_to_mm(zoom),
omega_deg=self.__devs.aerotech.omega,
smargon=self.__devs.smargon.readback,
beam_size_mm=self.__cfg.beam_size_mm,
aerotech=aerotech_coord,
aerotech_meas=self.__devs.abr_pos
)
return sample_geom
except Exception as e:
try:
test_x = self.__devs.gmx.value - abr_pos.x
print(f"Error creating sample geometry model: {e}")
print(f"Error not ABR, check smargon then restart server")
raise Exception(f"Error getting sample geometry {e}")
except:
print(f"Error getting sample geometry: {e}")
print(f"!!!!! ABR MAY HAVE DISCONNECTED !!!!!")
print(f"!!!!! CHECK ABR EPICS PANEL FOR ERRORS!!!!!")
print("!!!!! IF NOT RESTART GUI !!!!!")
print("CLOSING EXPERIMENTAL HUCTH FOR SAFETY")
self.__devs.exp_shutter.close()
return SampleGeometryModel(
beam_location_pxl=self.__cfg.beam_mark_coeff.apply(zoom),
pixel_in_mm=self.__cfg.pixel_to_mm(zoom),
omega_deg=self.__devs.aerotech.omega,
smargon=self.__devs.smargon.readback,
beam_size_mm=self.__cfg.beam_size_mm,
aerotech=Coordinate(x=0.0, y=0.0, z=0.0),
aerotech_meas=self.__devs.abr_pos
)
@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
@beam_size_mm.setter
def beam_size_mm(self, val: Coordinate):
self.__cfg.beam_size_mm = val
def get_beam_mark(self):
return self.__cfg.get_beam_mark(self.__devs.zoom)
def listen_changes(self) -> redis.client.PubSub:
return self.__cfg.listen_changes()
def __ml_bounding_box(self, sample_id: int | None = None, filename: str | None = None) -> RasterGridRequest | None:
time.sleep(0.2) # Just to be sure image is stable
#curr_image = self.camera_image
#box = self.__mlbox.predict(curr_image)
curr_image = cv2.cvtColor(self.camera_image, cv2.COLOR_RGB2BGR)
m = self.__mlbox.predict(curr_image, preferred_class=(3,0))
if m is None:
if filename is not None:
cv2.imwrite(f"{filename}_no_detection.jpg", curr_image)
self.__aare.upload_image(sample_id, f"{filename}_no_detection", curr_image)
return None
x1, y1, x2, y2 = m.box.top_x, m.box.top_y, m.box.bottom_x, m.box.bottom_y
if filename is not None:
cv2.rectangle(curr_image, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2)
cv2.imwrite(f"{filename}.jpg", curr_image)
self.__aare.upload_image(sample_id, filename, curr_image)
geom = self.sample_geometry
start_coord = geom.picture_to_smargon(Coordinate(x=x1, y=y1))
grid_size = Coordinate(x=geom.beam_size_mm.x * 0.8, y=geom.beam_size_mm.y * 0.8)
n_x = abs(ceil((x2 - x1) * geom.pixel_in_mm / grid_size.x))
n_y = abs(ceil((y2 - y1) * geom.pixel_in_mm / grid_size.y))
return RasterGridRequest(
exp_time_s=0.02,
transmission=1.0,
smargon_top_left = SmargonCoordinate(chi_deg = geom.smargon.chi_deg,
phi_deg= geom.smargon.phi_deg,
sh_mm=start_coord),
n_x=n_x,
n_y=n_y,
grid_size_mm= grid_size,
omega_deg=geom.omega_deg
)
def __ml_loop_centre_box(self, sample_id: int | None = None, filename: str | None = None) -> tuple[SmargonCoordinate | None, int| None, list[int] | None]:
#time.sleep(0.2) # Just to be sure image is stable
bgr_image = cv2.cvtColor(self.camera_image, cv2.COLOR_RGB2BGR)
boxes = self.__mlbox.predict_all_best(bgr_image, overlap_with_pin = 0.5, confidence_min=0.3)
if boxes is None:
if filename is not None:
self.__aare.upload_image(sample_id, f"{filename}_no_detection", bgr_image)
return None, None, None
classes: list[int] = []
pin = None
centre_x, centre_y = None, None
for box in boxes.boxes.values():
if box and box.cls is not None:
classes.append(int(box.cls.value))
if int(box.cls.value) == 1:
pin=box
best_box = self.__mlbox.get_preferred_class_box(boxes, (2,3,0,1))
if best_box is None or best_box.box is None or best_box.cls is None:
return None, None, classes if classes else None
cls = int(best_box.cls.value)
x1 = best_box.box.top_x
y1 = best_box.box.top_y
x2 = best_box.box.bottom_x
y2 = best_box.box.bottom_y
if filename is not None:
self.__aare.upload_image(sample_id, filename, bgr_image)
geom = self.sample_geometry
if cls == 0: # loop_all
if y1 + y2 <= x1 + x2:
centre_y = y1 + (y2 - y1) / 2
centre_x = x1
elif pin:
position_dict = self.__mlbox.check_box_relation(pin, best_box)
if position_dict["overlap_y"] and position_dict["overlap_x"]:
centre_y = y1 + (y2 - y1) / 2
centre_x = x1
cls = 1
logger.debug("significant overlap between pin and loop_all not picked up by ML")
else:
centre_y = y2 if position_dict["top"] else y1
centre_x = x1 + (x2 - x1) / 2
else:
centre_y = y1
centre_x = x1 + (x2 - x1) / 2
elif cls == 1: # pin
centre_y = y1 + (y2 - y1)/2
centre_x = x1
elif cls == 2 or cls == 3: #crystal or loop_face
centre_y = y1 + (y2 - y1)/2
centre_x = x1 + (x2 - x1)/2
else:
logger.debug(f"unknown box class {cls}")
return None, cls, classes if classes else None
coord = geom.picture_to_smargon(Coordinate(x=centre_x, y=centre_y))
return SmargonCoordinate(sh_mm=coord), cls, classes
def ml_bounding_box(self, sample_id: int | None = None, filename: str | None = None) -> RasterGridRequest | None:
try:
self.__cfg.try_set_busy(timeout=360)
r = self.__ml_bounding_box(sample_id, filename)
self.__cfg.state_busy = False
return r
except Exception:
self.__cfg.state_busy = False
raise
def face_detection(self, steps: int = 14, step_size: int = 15) -> dict:
self.__cfg.try_set_busy(timeout=360)
try:
logger.info("running face detection sequence")
result = self.__face_detection_sequence(steps=steps, step_size=step_size)
except Exception as e:
logger.error(f"error in face detection sequence {e}")
result = {
"samples": None,
"height_fit": None,
"area_fit": None,
}
self.__cfg.state_busy = False
return result
def face_detection_centre_correction(self, m:MLBoxModel, tolerance: float = 0.2):
geom = self.sample_geometry
beam_y = geom.beam_location_pxl.y
beam_x = geom.beam_location_pxl.x
x1 = m.box.top_x
y1 = m.box.top_y
y2 = m.box.bottom_y
centre_y = y1 + (y2 - y1) / 2
centre_x = x1
if beam_y !=0 and abs(centre_y-beam_y)/abs(beam_y) > tolerance:
coord = geom.picture_to_smargon(Coordinate(x=beam_x, y=centre_y))
self.__devs.smargon.target = SmargonCoordinate(sh_mm=coord)
self.__devs.smargon.wait(60)
return
def __face_detection_sequence(self, steps: int = 14, step_size: int = 15) -> dict:
self.__set_state(BeamlineStateEnum.SampleAlignment)
self.__devs.lamp_light = 2.5
self.__cfg.zoom_mode = ZoomModeEnum.LoopCenter
zoom_settings = self.__cfg.zoom_settings.z
zoom_value = self.__devs.zoom
logger.info('face detection sequence')
self.__devs.samcam_settings = SampleCameraSettings(
exposure=zoom_settings[zoom_value].exposure,
gain=zoom_settings[zoom_value].gain)
self.__devs.zoom_sync(zoom_value)
boxes: dict[int, tuple[float, float, float, float]] = {}
curr_angle = int(self.__devs.aerotech.omega)
total_range = steps * step_size + 1
start_angle = curr_angle if curr_angle + total_range < 720 else 0
end_angle = curr_angle + total_range
for angle in range(start_angle, end_angle, step_size):
logger.debug(f'moving to angle: {angle}')
rotate_time = time.perf_counter()
self.__devs.aerotech.move(angle, wait=True)
logger.info(f"time to rotate 15 degrees: {time.perf_counter() - rotate_time}")
curr_image = cv2.cvtColor(self.camera_image, cv2.COLOR_RGB2BGR)
box_time = time.perf_counter()
m = self.__mlbox.predict(curr_image, filename=None, preferred_class = (3,0))
logger.info(f"time to predict: {time.perf_counter() - box_time}")
if not m or not m.box:
logger.info(f"no box found for angle {angle}")
continue
cls_id = int(m.cls.value)
x1, y1, x2, y2 = m.box.top_x, m.box.top_y, m.box.bottom_x, m.box.bottom_y
self.face_detection_centre_correction(m, tolerance=0.2)
if cls_id == 3:
boxes[angle] = (x1, y1, x2, y2)
logger.info(f"accepted box at angle {angle}, cls={cls_id}, box={(x1, y1, x2, y2)}")
else:
logger.debug(f"ignoring class {cls_id} (pin/crystal) at angle {angle}")
if not boxes:
logger.info("no boxes found")
return {"samples": None, "height_fit": None, "area_fit": None}
best_fit_angle_area, area_params = fd.get_flat_face(boxes, start_angle, end_angle, True)
best_fit_angle_height, height_params = fd.get_flat_face(boxes, start_angle, end_angle, False)
fit_results = {"Area":{"angle":best_fit_angle_area, "params":area_params}, "Height": {"angle":best_fit_angle_height, "params":height_params}}
logger.info(f"best angle by area: {best_fit_angle_area}")
logger.info(f"best angle by height: {best_fit_angle_height}")
flat_face_angle, best_params, best_name = fd.choose_best_fit(fit_results)
logger.info(f"best params: {best_params}")
logger.info(f"chosen fit: {best_name}")
logger.info(f"best angle: {flat_face_angle}")
self.__devs.aerotech.move(flat_face_angle, wait=True)
samples_out = fd.get_samples_out(boxes)
logger.info(f"face detection sequence done, samples: {samples_out}")
return {
"samples": samples_out,
"height_fit": {"A": height_params["A"],
"B": height_params["B"],
"phi_rad": height_params["phi_rad"],
"C": height_params["C"],
"best_angle_deg": best_fit_angle_height
},
"area_fit": {"A": area_params["A"],
"B": area_params["B"],
"phi_rad": area_params["phi_rad"],
"C": height_params["C"],
"best_angle_deg": best_fit_angle_area
},
}
def __loop_center_sequence(self, sample_id: int | None = None) -> bool:
self.__set_state(BeamlineStateEnum.SampleAlignment)
self.__devs.lamp_light = 2.5
found_classes_count: dict[int, int] = {0:0, 1:0, 2:0, 3:0}
try:
self.__cfg.zoom_mode = ZoomModeEnum.LoopCenter
zoom_settings = self.__cfg.zoom_settings.z
for zoom_iter, zoom_value in enumerate(zoom_settings):
exposure = zoom_settings[zoom_value].exposure
gain = zoom_settings[zoom_value].gain
max_attempt = 2
attempt = 0
base_angles = (0, 45, 90) if (zoom_iter % 2 == 0) else (90, 45, 0)
if sample_id is not None:
self.save_screenshot_db(sample_id, f"pre_alc")
while attempt < max_attempt:
self.__devs.samcam_settings = SampleCameraSettings(exposure=exposure, gain=gain)
exp = int(exposure * 1000)
gn = int(gain)
self.__devs.zoom_sync(zoom_value)
logger.debug(f'zoom={zoom_value},gain={gn}, exp={exp}ms')
found_flag = False
found_angle: int | None = None
targets_found_this_attempt = 0
for angle in base_angles:
logger.debug(f"Moving to new omega: {angle}")
time_to_move_aerotech= time.perf_counter()
self.__devs.aerotech.move(angle, wait=True)
logger.info(f"time to move: {time.perf_counter()-time_to_move_aerotech}")
filename = f"{sample_id}_{angle}_{zoom_value:.0f}_{exp}_{gn}" if sample_id is not None else None
try:
target, cls, classes = self.__ml_loop_centre_box(sample_id, filename)
except Exception as e:
logger.error(f"Error getting ML box for angle {angle}")
logger.error(f"Exception: {e}")
target, cls, classes = None, None, None
if target is None:
logger.debug("no target found")
continue
if classes:
for c in classes:
found_classes_count[int(c)] += 1
logger.debug(f"classes found: {classes}")
logger.debug(f"class found: {cls}")
if cls is not None and cls != 1:
targets_found_this_attempt += 1
found_flag = True
found_angle = angle
time_to_move_smargon = time.perf_counter()
self.__devs.smargon.target = target
self.__devs.smargon.wait(60)
logger.info(f"time to move smargon: {time.perf_counter() - time_to_move_smargon}")
if targets_found_this_attempt == 0:
logger.error(f"No targets found in this attempt {attempt}")
raise LoopCenteringFailed
else:
if targets_found_this_attempt >= len(base_angles):
logger.debug(f"sucessfully found {targets_found_this_attempt} targets in attempt {attempt + 1} ")
break
if found_flag is not None and found_angle is not None:
logger.debug(f"found a target at angle {found_angle} in attempt {attempt + 1}")
base_angles = (found_angle, found_angle + 45, found_angle + 90)
logger.debug(f"new base angles: {base_angles}")
attempt += 1
logger.debug(f"attempt {attempt} of {max_attempt}")
if attempt >= max_attempt:
logger.error(f"{attempt} exceeds max attempts {max_attempt}")
raise LoopCenteringFailed
#i += 1
logger.debug("alc success")
self.__aare.sample_centered(self.__cfg.current_sample)
if sample_id is not None:
self.save_screenshot_db(sample_id, f"{sample_id}_centered")
return True
except Exception as e:
total_detections = sum(found_classes_count.values())
if total_detections == 0:
logger.error("ALC: No objects found in any zoom")
self.__aare.alc_failed(self.sample, alc_comment="No objects detected")
else:
self.__aare.alc_failed(
self.sample,
alc_comment=(
"Failed to centre but detected objects - "
f"Crystal: {found_classes_count.get(2,0)}, "
f"Loop_face: {found_classes_count.get(3,0)}, "
f"Loop_all: {found_classes_count.get(0,0)}, "
f"Pin: {found_classes_count.get(1,0)}"
),
)
logger.error("Failed to centre but detected objects - "
f"Crystal: {found_classes_count.get(2,0)}, "
f"Loop_face: {found_classes_count.get(3,0)}, "
f"Loop_all: {found_classes_count.get(0,0)}, "
f"Pin: {found_classes_count.get(1,0)}")
logger.info(f"Error in loop centering: {e}")
return False
def auto_loop_center(self, sample_id: int | None = None) -> float:
start = time.perf_counter()
try:
self.__cfg.try_set_busy(timeout=360)
if not self.__loop_center_sequence(sample_id):
raise LoopCenteringFailed
self.__cfg.state_busy = False
except Exception:
self.__cfg.zoom_mode = ZoomModeEnum.User
self.__devs.samcam_settings = self.__cfg.zoom_settings.get_camera_settings(self.zoom)
self.__cfg.state_busy = False
raise
self.__cfg.zoom_mode = ZoomModeEnum.User
self.__devs.samcam_settings = self.__cfg.zoom_settings.get_camera_settings(self.zoom)
end = time.perf_counter()
return end - start
def save_screenshot(self, filename: str):
#time.sleep(0.2) # Wait 200 ms to ensure camera image is stable
bgr_image = cv2.cvtColor(self.camera_image, cv2.COLOR_RGB2BGR)
cv2.imwrite(f"{filename}.jpg", bgr_image)
def save_screenshot_db(self, sample_id: int, filename: str):
#time.sleep(0.2) # Wait 200 ms to ensure camera image is stable
bgr_image = cv2.cvtColor(self.camera_image, cv2.COLOR_RGB2BGR)
self.__aare.upload_image(sample_id, filename, bgr_image)
@property
def sample_spreadsheet(self) -> SampleShortInfoList:
return self.__cfg.spreadsheet
@property
def reference_tools(self) -> SampleShortInfoList:
return self.__cfg.reference_tools
def sample_spreadsheet_user(self, pgroup: str) -> SampleShortInfoList:
sample = self.sample_spreadsheet
sample.s = list(filter(lambda x: x.user == pgroup, sample.s))
return sample
def get_auto_raster_params(self) -> SimpleScanParameters:
if self.status.sample is None:
return SimpleScanParameters(dtz=150, exp_time_s=0.04, transmission=1.0)
aaredb_params = self.status.sample.aaredb_params if hasattr(self.status.sample, "aaredb_params") else None
if aaredb_params is None:
return SimpleScanParameters(dtz=150, exp_time_s=0.04, transmission=1.0)
params = SimpleScanParameters()
# Exposure
exp = getattr(aaredb_params, 'exposure', None)
if exp is not None:
params.exp_time_s = exp
else:
params.exp_time_s = 0.04 # Default
# Transmission
trans = getattr(aaredb_params, 'transmission', None)
if trans is not None:
logger.debug(f"transmission: {trans}")
params.transmission = trans / 100.0 if trans > 1.0 else trans
else:
params.transmission = 1.0 # Default
# Resolution and DTZ
res = getattr(aaredb_params, 'targetresolution', None)
if res is not None:
logger.debug(f"resolution: {res}")
try:
params.dtz = self.diffraction_geometry.calc_dtz_mm(res)
logger.debug(f"dtz: {params.dtz}")
except Exception as e:
logger.error(f"Failed to calculate dtz for resolution {res}: {e}")
params.dtz = 150 # Fallback default
# Adjust exposure time based on resolution
if res <= 1.5:
params.exp_time_s = 0.02
elif 1.5 < res <= 3.0:
params.exp_time_s = 0.04
else:
params.exp_time_s = 0.08
else:
params.dtz = 150
params.exp_time_s = 0.04
return params
def get_collection_params(self, prefer_smart: bool = False) -> tuple[SimpleScanParameters, str]:
spreadsheet_params, file_prefix = self.spreadsheet_params()
logger.debug(f"spreadsheet_params: {spreadsheet_params}")
smart_params = self.__cfg.auto_params
default_params = SimpleScanParameters(exp_time_s=0.04, dtz=110, incr_omega_deg=0.2)
#if file_prefix is not None:
# default_params.file_prefix = file_prefix
#self.__aare.send_msg_to_db(self.sample,event_type=SampleEventType(''), comment=f'smart_params: {smart_params}')
if prefer_smart:
if smart_params:
#if file_prefix:
# smart_params.file_prefix = file_prefix
return smart_params, "smart_params"
if spreadsheet_params:
return spreadsheet_params, "spreadsheet_params"
else:
if spreadsheet_params:
return spreadsheet_params, "spreadsheet_params"
if smart_params:
#if file_prefix:
# smart_params.file_prefix = file_prefix
return smart_params, "smart_params"
return default_params, "defaults"
def measure(self, sample: SampleShortInfo) -> float:
start = time.perf_counter()
formatted_date = datetime.now().strftime('%Y%m%d')
sample_prefix = "{}/{}/{:02d}/{}".format(
formatted_date,
sample.puck_name,
sample.pin,
sample.sample_name
)
try:
logger.info(f"setting busy at {time.perf_counter() - start}")
self.__cfg.try_set_busy(timeout=360)
logger.info(f"set busy at {time.perf_counter() - start}")
geom = self.sample_geometry
start_mount=time.perf_counter()
logger.info(f"starting mount {sample.db_id} at {time.ctime()}")
self.__mount(sample)
logger.info(f"mounting done at {time.perf_counter() - start_mount}, total time: {time.perf_counter() - start}")
alc_time = time.perf_counter() - start
logger.info(f"starting alc at {alc_time}")
if not self.__loop_center_sequence(sample.db_id):
self.__aare.alc_failed(sample)
print("alc failed")
self.__cfg.state_busy = False
end = time.perf_counter()
return end - start
#raise LoopCenteringFailed
logger.info(f"alc done at {time.perf_counter() - start}")
self.__face_detection_sequence()
logger.info(f"face_detection done at {time.perf_counter() - start}")
self.zoom = 500
hex_string = secrets.token_hex(3) # 3 bytes = 6 hex characters
print(hex_string.upper())
raster_params = self.get_auto_raster_params()
if self.__auto_center(RasterGridRequest(
exp_time_s=raster_params.exp_time_s,
file_prefix=sample_prefix + f"_{hex_string}",
smargon_top_left= SmargonCoordinate(),
n_x=1,
n_y=1,
dtz=raster_params.dtz,
grid_size_mm=Coordinate(x=geom.beam_size_mm.x * 0.5, y=geom.beam_size_mm.y * 0.5),
omega_deg=self.omega,
transmission = raster_params.transmission,
)):
logger.info(f"raster scans done at {time.perf_counter() - start}")
params, source = self.get_collection_params(prefer_smart=False)
logger.info(f"Using {source} for data collection: {params}")
self.__cfg.zoom_mode = ZoomModeEnum.User
self.__devs.samcam_settings = self.__cfg.zoom_settings.get_camera_settings(self.zoom)
self.__devs.dtz.move(params.dtz, wait=True)
if self.omega + 180 < 720:
start_omega = self.omega
else:
start_omega = params.start_omega_deg
self.__rotation( RotationScanRequest(start_omega_deg=start_omega,
dtz=params.dtz,
file_prefix="data/" + sample_prefix + f"_{hex_string}",
exp_time_s=params.exp_time_s,
incr_omega_deg=params.incr_omega_deg,
steps=params.steps,
transmission=params.transmission,
))
logger.info(f"rotation done at {time.perf_counter() - start}")
else:
logger.error("auto center failed")
self.__aare.axc_failed(sample)
self.zoom = 1
self.__cfg.zoom_mode = ZoomModeEnum.User
self.__devs.samcam_settings = self.__cfg.zoom_settings.get_camera_settings(self.zoom)
self.__cfg.state_busy = False
except Exception as e:
logger.error(f"Error in measure: {e}")
self.__aare.sample_failed(sample)
self.__cfg.state_busy = False
end = time.perf_counter()
return end - start
#raise
end = time.perf_counter()
return end - start
def __set_state(self, target: BeamlineStateEnum):
# __set_state assumes that beamline is already in busy state
# it will apply a proper transformation and change state afterward
# specifically:
# 1. If target state is maintenance, just go there
# 2. If target state is same as current, nothing will happen
# 3. If target state cannot be reached, exception is raised and current state is kept
# 4. If exception is raised during transformation, state is set to maintenance
# 5. If transformation goes OK, target state is set
#
# Busy state will be cleared only, if exception is raised.
#
curr_state = self.__cfg.state
if not self.__cfg.state_busy:
raise Exception("Beamline should be busy")
if target == BeamlineStateEnum.Maintenance:
self.__cfg.state = BeamlineStateEnum.Maintenance
elif target != curr_state:
self.__cfg.state = BeamlineStateEnum.Moving
try:
match curr_state:
case BeamlineStateEnum.Maintenance:
if target == BeamlineStateEnum.SampleExchange:
workflows.m2se(self.__devs, self.__cfg)
else:
raise TransformationInvalidException()
case BeamlineStateEnum.SampleExchange:
if target == BeamlineStateEnum.SampleAlignment:
workflows.se2sa(self.__devs, self.__cfg)
elif target == BeamlineStateEnum.BeamLocation:
workflows.se2sa(self.__devs, self.__cfg)
workflows.sa2bl(self.__devs, self.__cfg)
else:
raise TransformationInvalidException()
case BeamlineStateEnum.DewarTransfer:
if target == BeamlineStateEnum.SampleAlignment:
workflows.dh2sa(self.__devs, self.__cfg)
else:
raise TransformationInvalidException()
case BeamlineStateEnum.DataCollection:
if target == BeamlineStateEnum.SampleAlignment:
workflows.dc2sa(self.__devs, self.__cfg)
elif target == BeamlineStateEnum.RobotSampleExchange:
workflows.dc2rse(self.__devs, self.__cfg)
else:
raise TransformationInvalidException()
case BeamlineStateEnum.BeamLocation:
if target == BeamlineStateEnum.SampleAlignment:
workflows.bl2sa(self.__devs, self.__cfg)
elif target == BeamlineStateEnum.SampleExchange:
workflows.bl2sa(self.__devs, self.__cfg)
workflows.sa2se(self.__devs, self.__cfg)
else:
raise TransformationInvalidException()
case BeamlineStateEnum.SampleAlignment:
if target == BeamlineStateEnum.DewarTransfer:
workflows.sa2dh(self.__devs, self.__cfg)
elif target == BeamlineStateEnum.SampleExchange:
workflows.sa2se(self.__devs, self.__cfg)
elif target == BeamlineStateEnum.RobotSampleExchange:
workflows.sa2rse(self.__devs, self.__cfg)
elif target == BeamlineStateEnum.DataCollection:
workflows.sa2dc(self.__devs, self.__cfg)
elif target == BeamlineStateEnum.XrayFluorescence:
workflows.sa2xrf(self.__devs, self.__cfg)
elif target == BeamlineStateEnum.BeamLocation:
workflows.sa2bl(self.__devs, self.__cfg)
else:
raise TransformationInvalidException()
case BeamlineStateEnum.XrayFluorescence:
if target == BeamlineStateEnum.SampleAlignment:
workflows.xrf2sa(self.__devs, self.__cfg)
else:
raise TransformationInvalidException()
case BeamlineStateEnum.RobotSampleExchange:
if target == BeamlineStateEnum.SampleAlignment:
workflows.rse2sa(self.__devs, self.__cfg)
else:
raise TransformationInvalidException()
self.__cfg.state = target
except TransformationInvalidException as e:
self.__cfg.state = curr_state
self.__cfg.state_busy = False
raise e
except Exception as e:
self.__cfg.state = BeamlineStateEnum.Maintenance
self.__cfg.state_busy = False
raise e
@property
def shutter(self) -> bool:
return self.__devs.shutter
@shutter.setter
def shutter(self, v: bool):
self.__devs.shutter = v
@property
def diffraction_geometry(self) -> DiffractionGeometry:
det_cfg = self.__jfjoch.detector()
return DiffractionGeometry(
energy_keV=self.__devs.energy_kev,
dtz_mm=self.__devs.dtz.value,
detector_size_pxl=(det_cfg.width, det_cfg.height),
pixel_size_mm=det_cfg.pixel_size_mm,
beam_center_pxl=self.__cfg.beam_center,
detector_description=det_cfg.description,
detector_serial_number=det_cfg.serial_number,
poni_rot1_rad=-0.001396263,
poni_rot2_rad=-0.003839724,
)
@property
def beamline_status(self) -> BeamlineStatus:
return BeamlineStatus(
ring_current_mA=self.__devs.ring_current,
light=self.light,
cryojet_K=self.__devs.cryojet_temp,
shutter_open=self.__devs.shutter,
exp_shutter_open=self.__devs.exp_shutter.state(),
flux_ph_s=self.__devs.full_flux,
sample_camera=self.__devs.samcam_settings,
name=self.__bl,
transmission=self.__devs.transmission.get(),
zoom=self.__devs.zoom,
commissioning_mode=self.__cfg.commissioning_mode,
dtz_min=self.__devs.dtz_low,
dtz_max=self.__devs.dtz_high,
)
@property
def status(self) -> DAQStatusModel:
# session should be set by FastAPI server
return DAQStatusModel(
state=self.state,
busy=self.busy,
geom=self.sample_geometry,
bl=self.beamline_status,
sample=self.sample,
session=SessionStatus(),
diffraction=self.diffraction_geometry,
box=self.__saved_box,
last_best_res = self.__cfg.last_best_res,
last_best_b_factor = self.__cfg.last_best_b_factor,
crystal_size = self.__cfg.crystal_size
)
def cancel(self):
if self.__cfg.state == BeamlineStateEnum.DataCollection:
self.__devs.aerotech.stop()
self.__jfjoch.cancel()
def anneal(self, time_s: float):
self.__cfg.set_busy(BeamlineStateEnum.SampleAlignment)
try:
self.__devs.anneal(time_s)
finally:
self.__cfg.state_busy = False
def fluorimeter_take_spectrum(self, fm: FluorescenceSpectrumParameterModel) -> FluorescenceSpectrumOutputModel:
self.__cfg.try_set_busy(timeout=360)
try:
self.__set_state(BeamlineStateEnum.XrayFluorescence)
if fm.transmission is not None:
self.__devs.transmission.set(fm.transmission, wait=True)
self.__devs.fluorimeter.start_acquisition(erase=fm.erase)
self.__devs.aerotech.set_shutter(1)
time.sleep(fm.acq_time_s)
self.__devs.aerotech.set_shutter(0)
self.__devs.fluorimeter.stop_acquisition()
time.sleep(0.2)
spectrum = self.__devs.fluorimeter.get_current_data()
offset = self.__devs.fluorimeter.offset
slope = self.__devs.fluorimeter.slope # slope is in keV!
energy = [slope * 1000.0 * i + offset for i in range(len(spectrum))]
fluo_output = FluorescenceSpectrumOutputModel(spectrum=spectrum,
bkg=self.__devs.fluorimeter.get_current_background(),
energy_eV=energy,
average_dead_time=self.__devs.fluorimeter.average_dead_time() / 100.0)
if self.sample is not None:
self.__cfg.xrf = fluo_output
self.__set_state(BeamlineStateEnum.SampleAlignment)
self.__cfg.state_busy = False
return fluo_output
except Exception as e:
self.__set_state(BeamlineStateEnum.SampleAlignment)
self.__cfg.state_busy = False
raise e
def fluorimeter_start(self, erase: bool = False):
self.__devs.aerotech.set_shutter(1)
self.__devs.fluorimeter.start_acquisition(erase=erase)
def fluorimeter_stop(self):
self.__devs.fluorimeter.stop_acquisition()
self.__devs.aerotech.set_shutter(0)
def fluorimeter_status(self) -> int | None:
return self.__devs.fluorimeter.check_status()
def fluorimeter_data(self):
return self.__devs.fluorimeter.get_current_data()
def fluorimeter_background(self):
return self.__devs.fluorimeter.get_current_background()