# encoding: utf-8
#
# Project name: MXCuBE
# https://github.com/mxcube
#
# This file is part of MXCuBE software.
#
# MXCuBE is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# MXCuBE is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with MXCuBE. If not, see <http://www.gnu.org/licenses/>.
"""
Detector API
Abstract methods:
prepare_acquisition
start_acquisition
has_shutterless
Overloaded methods:
force_emit_signals
Implemented methods:
stop_acquisition
get_pixel_size, get_width, get_heigth, get_metadata
get_radius, get_outer_radius
get_beam_position
get_roi_mode, set_roi_mode, get_roi_mode_name, get_roi_modes
get_exposure_time_limits
get_threshold_energy, set_threshold_energy
get_binning_mode, set_binning_mode
Implemented propertries:
distance
Emitted signals:
detectorRoiModeChanged
temperatureChanged
humidityChanged
expTimeLimitsChanged
frameRateChanged
stateChanged
specificStateChanged
Hardware objects used: energy
"""
import abc
import ast
import math
from mxcubecore.BaseHardwareObjects import HardwareObject
from mxcubecore.model.queue_model_objects import PathTemplate
__copyright__ = """ Copyright © 2019 by the MXCuBE collaboration """
__license__ = "LGPLv3+"
[docs]class AbstractDetector(HardwareObject):
"""Common base class for detectors"""
__metaclass__ = abc.ABCMeta
def __init__(self, name):
super().__init__(name)
self._temperature = None
self._humidity = None
self._actual_frame_rate = None
self._pixel_size = (None, None)
self._binning_mode = None
self._roi_mode = 0
self._images_per_file = 0
self._roi_modes_list = []
self._exposure_time_limits = (None, None)
self._threshold_energy = None
self._distance_motor_hwobj = None
self._width = None # [pixel]
self._height = None # [pixel]
self._metadata = {}
[docs] def init(self):
"""Initialise some common parameters"""
super().init()
self._metadata = self.get_property("beam", {})
self._images_per_file = self.get_property("images_per_file", 100)
self._distance_motor_hwobj = self.get_object_by_role("detector_distance")
self._roi_modes_list = ast.literal_eval(self.get_property("roiModes", "()"))
self._pixel_size = (self.get_property("px"), self.get_property("py"))
self._width = self.get_property("width")
self._height = self.get_property("height")
min_exp_time = self.get_property("minimum_exposure_time", None)
max_exp_time = self.get_property("maximum_exposure_time", None)
if min_exp_time is None:
self.log.warning("Minimum exposure time not set for detector, using None")
else:
min_exp_time = float(min_exp_time)
if max_exp_time is None:
self.log.warning("Maximum exposure time not set for detector, using None")
else:
max_exp_time = float(max_exp_time)
self._exposure_time_limits = (
min_exp_time,
max_exp_time,
)
[docs] def force_emit_signals(self):
"""Emit all hardware object signals."""
self.emit("detectorRoiModeChanged", (self._roi_mode,))
self.emit("temperatureChanged", (self._temperature, True))
self.emit("humidityChanged", (self._humidity, True))
self.emit("expTimeLimitsChanged", (self._exposure_time_limits,))
self.emit("frameRateChanged", (self._actual_frame_rate,))
self.emit("stateChanged", (self._state,))
self.emit("specificStateChanged", (self._specific_state,))
@property
def images_per_file(self):
return self._images_per_file
@property
def distance(self):
"""Property for contained detector_distance hardware object
Returns:
(AbstratctMotor): Hardware object.
"""
return self._distance_motor_hwobj
[docs] @abc.abstractmethod
def has_shutterless(self):
"""Check if detector is capable of shutterless acquisition.
Returns:
(bool): True if detector is capable, False otherwise
"""
return
[docs] @abc.abstractmethod
def prepare_acquisition(self, *args, **kwargs):
"""
Prepares detector for acquisition
"""
[docs] def last_image_saved(self):
"""Get the path to the last saved image
Returns:
(str): full path.
"""
return None
[docs] @abc.abstractmethod
def start_acquisition(self):
"""Start the acquisition."""
[docs] def stop_acquisition(self):
"""Stop the acquisition."""
[docs] def get_roi_mode(self):
"""Get the current ROI mode.
Returns:
(str): current ROI mode
"""
return self._roi_mode
[docs] def set_roi_mode(self, roi_mode):
"""Set the current ROI mode.
Args:
roi_mode (int): ROI mode to set.
"""
self._roi_mode = roi_mode
self.emit("detectorRoiModeChanged", (self._roi_mode,))
[docs] def get_roi_mode_name(self):
"""Get the current ROI mode name.
Returns:
(str): name
"""
return self._roi_modes_list[self._roi_mode]
[docs] def get_roi_modes(self):
"""Get the available ROI modes
Returns:
(tuple): Tuple of strings.
"""
return tuple(self._roi_modes_list)
[docs] def get_exposure_time_limits(self):
"""Get the exposure time lower and upper limits.
Returns:
(tuple): Two floats (lower and upper limit) [s]
"""
return self._exposure_time_limits
[docs] def get_pixel_size(self):
"""Get the pixel size.
Returns:
(tuple): Two floats (x, y) [mm].
"""
return self._pixel_size
[docs] def get_binning_mode(self):
"""Get the current binning mode.
Returns:
(int): Binning mode
"""
return self._binning_mode
[docs] def set_binning_mode(self, value):
"""Set the current binning mode.
Args:
value (int): binning mode.
"""
self._binning_mode = value
[docs] def get_beam_position(self, distance=None, wavelength=None): # noqa: ARG002
"""Calculate the beam position for a given distance.
Args:
distance (float): detector distance [mm]
wavelength (float): X-ray wavelength [Å]
Returns:
tuple(float, float): Beam position x,y coordinates [pixel].
"""
# Following is just an example of calculating beam center using
# simple linear regression,
# One needs to overload the method in case more complex calculation
# is required, e.g. using other detector positioning motors and
# wavelength
try:
distance = (
distance
if distance is not None
else self._distance_motor_hwobj.get_value()
)
metadata = self.get_metadata()
beam_position = (
float(distance * metadata["ax"] + metadata["bx"]),
float(distance * metadata["ay"] + metadata["by"]),
)
except (AttributeError, KeyError):
beam_position = (None, None)
return beam_position
[docs] def get_radius(self, distance=None):
"""Get distance from the beam position to the nearest detector edge.
Args:
distance (float): Distance [mm]
Returns:
(float): Detector radius [mm]
"""
try:
distance = (
distance
if distance is not None
else self._distance_motor_hwobj.get_value()
)
except AttributeError as err:
raise RuntimeError("Cannot calculate radius, unknown distance") from err
beam_x, beam_y = self.get_beam_position(distance)
pixel_x, pixel_y = self.get_pixel_size()
rrx = min(self.get_width() - beam_x, beam_x) * pixel_x
rry = min(self.get_height() - beam_y, beam_y) * pixel_y
radius = min(rrx, rry)
return radius
[docs] def get_outer_radius(self, distance=None):
"""Get distance from beam_position to the furthest point on the detector.
Args:
distance (float): Distance [mm]
Returns:
(float): Detector outer adius [mm]
"""
try:
distance = (
distance
if distance is not None
else self._distance_motor_hwobj.get_value()
)
except AttributeError as err:
raise RuntimeError(
"Cannot calculate outer radius, distance unknown"
) from err
beam_x, beam_y = self.get_beam_position(distance)
pixel_x, pixel_y = self.get_pixel_size()
max_delta_x = max(beam_x, self._width - beam_x) * pixel_x
max_delta_y = max(beam_y, self._height - beam_y) * pixel_y
return math.sqrt(max_delta_x * max_delta_x + max_delta_y * max_delta_y)
[docs] def get_width(self):
"""Returns detector width.
Returns:
(int): detector width [px]
"""
return self._width
[docs] def get_height(self):
"""Returns detector height.
Returns:
(int): detector height [px]
"""
return self._height
[docs] def set_threshold_energy(self, threshold_energy):
"""
Args:
threshold_energy (float): Detector threshold energy [eV]
"""
self._threshold_energy = threshold_energy
[docs] def get_threshold_energy(self):
"""Returns detector threshold_energy
Returns:
(float): Detector threshold energy [eV]
"""
return self._threshold_energy
def get_image_file_name(self, path_template, suffix=None):
template = "%s_%s_%%0" + str(path_template.precision) + "d.%s"
suffix = suffix or path_template.suffix
file_name = template % (
path_template.get_prefix(),
path_template.run_number,
suffix,
)
if path_template.compression:
file_name = "%s.gz" % file_name
return file_name
[docs] def get_first_and_last_file(self, pt: PathTemplate):
"""
Get complete path to first and last image
Args:
pt (PathTempalte): Path template parameter
Returns:
(Tuple): Tuple containing first and last image path (first, last)
"""
start_num = pt.start_num
end_num = pt.start_num + pt.num_files - 1
return (pt.get_image_path() % start_num, pt.get_image_path() % end_num)