Merge branch 'master' of gitlab.psi.ch:/bec/ophyd_devices

This commit is contained in:
2022-07-25 14:01:10 +02:00
4 changed files with 303 additions and 60 deletions

View File

@@ -1,7 +1,7 @@
# This file is a template, and might need editing before it works on your project.
# Official language image. Look for the different tagged releases at:
# https://hub.docker.com/r/library/python/tags/
image: "python:3.9"
image: "python:3.8"
#commands to run in the Docker container before starting each job.
before_script:
- pip install -r ./requirements.txt
@@ -12,4 +12,7 @@ stages:
pytest:
stage: Test
script:
- apt-get update && apt-get install -y git
- git clone https://oauth2:$CI_BEC_KEY@gitlab.psi.ch/bec/bec.git
- pip install -e ./bec/bec_utils
- pytest -v ./tests

View File

@@ -6,9 +6,9 @@ from typing import List
import numpy as np
from ophyd import Component as Cpt
from ophyd import Device, PositionerBase
from ophyd import Device, PositionerBase, Signal
from ophyd.status import wait as status_wait
from ophyd.utils import ReadOnlyError
from ophyd.utils import ReadOnlyError, LimitError
from ophyd_devices.utils.controller import Controller, threadlocked
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
from prettytable import PrettyTable
@@ -29,7 +29,7 @@ class BECConfigError(Exception):
def retry_once(fcn):
"""Decorator to rerun a function in case a SmaractCommunicationError was raised. This may happen if the buffer was not empty."""
"""Decorator to rerun a function in case a Galil communication error was raised. This may happen if the buffer was not empty."""
@functools.wraps(fcn)
def wrapper(self, *args, **kwargs):
@@ -49,6 +49,7 @@ class GalilController(Controller):
"galil_show_all",
"socket_put_and_receive",
"socket_put_confirmed",
"lgalil_is_air_off_and_orchestra_enabled"
]
def __init__(
@@ -287,6 +288,15 @@ class GalilSetpointSignal(GalilSignalBase):
if axes_referenced:
while self.controller.is_thread_active(0):
time.sleep(0.1)
if self.parent.axis_Id_numeric == 2:
angle_status = self.parent.device_manager.devices[
self.parent.rt
].obj.controller.feedback_status_angle_lamni()
if angle_status:
self.controller.socket_put_confirmed("angintf=1")
self.controller.socket_put_confirmed(f"naxis={self.parent.axis_Id_numeric}")
self.controller.socket_put_confirmed(f"ntarget={target_val:.3f}")
self.controller.socket_put_confirmed("movereq=1")
@@ -338,6 +348,8 @@ class GalilMotor(Device, PositionerBase):
motor_resolution = Cpt(GalilMotorResolution, signal_name="resolution", kind="config")
motor_is_moving = Cpt(GalilMotorIsMoving, signal_name="motor_is_moving", kind="normal")
all_axes_referenced = Cpt(GalilAxesReferenced, signal_name="all_axes_referenced", kind="config")
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
SUB_READBACK = "readback"
SUB_CONNECTION_CHANGE = "connection_change"
@@ -355,6 +367,7 @@ class GalilMotor(Device, PositionerBase):
parent=None,
host="mpc2680.psi.ch",
port=8081,
limits=None,
sign=1,
socket_cls=SocketIO,
device_manager=None,
@@ -390,6 +403,30 @@ class GalilMotor(Device, PositionerBase):
self._update_connection_state()
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
if limits is not None:
assert len(limits) == 2
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())
@property
def low_limit(self):
return self.limits[0]
@property
def high_limit(self):
return self.limits[1]
def check_value(self, pos):
"""Check that the position is within the soft limits"""
low_limit, high_limit = self.limits
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
raise LimitError(f"position={pos} not within limits {self.limits}")
def _update_connection_state(self, **kwargs):
for walk in self.walk_signals():
walk.item._metadata["connected"] = self.controller.connected
@@ -429,7 +466,7 @@ class GalilMotor(Device, PositionerBase):
If motion fails other than timing out
"""
self._started_moving = False
timeout = kwargs.pop("timeout", 4)
timeout = kwargs.pop("timeout", 100)
status = super().move(position, timeout=timeout, **kwargs)
self.user_setpoint.put(position, wait=False)

View File

@@ -11,11 +11,13 @@ from ophyd_devices.utils.socket import (
raise_if_disconnected,
SocketSignal,
)
import logging
from ophyd.utils import ReadOnlyError
from bec_utils import bec_logger
from ophyd.utils import ReadOnlyError,LimitError
from ophyd.status import wait as status_wait
from ophyd import Signal
logger = logging.getLogger("rtlamni")
logger = bec_logger.logger
def threadlocked(fcn):
"""Ensure that thread acquires and releases the lock."""
@@ -34,6 +36,8 @@ class RtLamniCommunicationError(Exception):
class RtLamniError(Exception):
pass
class BECConfigError(Exception):
pass
def retry_once(fcn):
"""Decorator to rerun a function in case a CommunicationError was raised. This may happen if the buffer was not empty."""
@@ -54,7 +58,15 @@ class RtLamniController(Controller):
"socket_put_and_receive",
"set_rotation_angle",
"feedback_disable",
"feedback_enable_without_reset"
"feedback_enable_without_reset",
"feedback_disable_and_even_reset_lamni_angle_interferometer",
"feedback_enable_with_reset",
"add_pos_to_scan",
"clear_trajectory_generator",
"_set_axis_velocity",
"_set_axis_velocity_maximum_speed",
"_position_sampling_single_read",
"_position_sampling_single_reset_and_start_sampling"
]
def __init__(
self,
@@ -69,6 +81,7 @@ class RtLamniController(Controller):
if not hasattr(self, "_initialized") or not self._initialized:
self._rtlamni_axis_per_controller = 3
self._axis = [None for axis_num in range(self._rtlamni_axis_per_controller)]
self._min_scan_buffer_reached = False
super().__init__(
name=name,
socket=socket,
@@ -78,13 +91,18 @@ class RtLamniController(Controller):
kind=kind,
)
def on(self, controller_num=0) -> None:
"""Open a new socket connection to the controller"""
if not self.connected:
try:
self.sock.open()
#discuss - after disconnect takes a while for the server to be ready again
welcome_message = self.sock.receive()
self.connected = True
except ConnectionRefusedError as conn_error:
logger.error("Failed to open a connection to RTLamNI.")
else:
logger.info("The connection has already been established.")
# warnings.warn(f"The connection has already been established.", stacklevel=2)
@@ -140,8 +158,8 @@ class RtLamniController(Controller):
def set_rotation_angle(self, val:float):
self.socket_put(f"a{(val-300+30.538)/180*np.pi}")
def stop_all_axes(self) -> str:
return 0 #self.socket_put_and_receive(f"XQ#STOP,1")
def stop_all_axes(self):
self.socket_put("sc")
def feedback_disable(self):
self.socket_put("J0")
@@ -152,6 +170,25 @@ class RtLamniController(Controller):
#motor_par("lopty","disable",0)
#motor_par("loptz","disable",0)
def _set_axis_velocity(self, um_per_s):
self.socket_put(f"V{um_per_s}")
def _set_axis_velocity_maximum_speed(self):
self.socket_put(f"V0")
#for developement of soft continuous scanning
def _position_sampling_single_reset_and_start_sampling(self):
self.socket_put(f"Ss")
def _position_sampling_single_read(self):
(number_of_samples,sum0,sum0_2,sum1,sum1_2,sum2,sum2_2) = self.socket_put_and_receive(f"Sr").split(",")
avg_x = float(sum1)/int(number_of_samples)
avg_y = float(sum0)/int(number_of_samples)
stdev_x = np.sqrt(float(sum1_2)/int(number_of_samples)-np.power(float(sum1)/int(number_of_samples),2))
stdev_y = np.sqrt(float(sum0_2)/int(number_of_samples)-np.power(float(sum0)/int(number_of_samples),2))
return(avg_x,avg_y,stdev_x,stdev_y)
def feedback_enable_without_reset(self):
#read current interferometer position
return_table = (self.socket_put_and_receive(f"J4")).split(",")
@@ -167,6 +204,7 @@ class RtLamniController(Controller):
#motor_par("loptx","disable",1)
#motor_par("lopty","disable",1)
#motor_par("loptz","disable",1)
#umv rtx x_curr rty y_curr
def feedback_disable_and_even_reset_lamni_angle_interferometer(self):
self.socket_put("J6")
@@ -177,69 +215,181 @@ class RtLamniController(Controller):
#motor_par("lopty","disable",0)
#motor_par("loptz","disable",0)
def get_device_manager(self):
for axis in self._axis:
if hasattr(axis, 'device_manager') and axis.device_manager:
return axis.device_manager
raise BECConfigError('Could not access the device_manager')
def get_axis_by_name(self, name):
for axis in self._axis:
if axis:
if axis.name == name:
return axis
raise RuntimeError(f'Could not find an axis with name {name}')
def clear_trajectory_generator(self):
self.socket_put("sc")
logger.info("LamNI scan stopped and deleted, moving to start position")
def add_pos_to_scan(self, positions) -> None:
def send_positions(parent, positions):
parent._min_scan_buffer_reached = False
for pos_index, pos in enumerate(positions):
parent.socket_put_and_receive(f"s{pos[0]},{pos[1]},0")
if pos_index > 100:
parent._min_scan_buffer_reached = True
parent._min_scan_buffer_reached = True
threading.Thread(target=send_positions, args=(self, positions), daemon=True).start()
@retry_once
def get_scan_status(self):
return_table = (self.socket_put_and_receive(f"sr")).split(",")
if len(return_table)!=3:
raise RtLamniCommunicationError(f"Expected to receive 3 return values. Instead received {return_table}")
mode=int(return_table[0])
#mode 0: direct positioning
#mode 1: running internal timer (not tested/used anymore)
#mode 2: rt point scan running
#mode 3: rt point scan starting
#mode 5/6: rt continuous scanning (not available in LamNI)
number_of_positions_planned=int(return_table[1])
current_position_in_scan=int(return_table[2])
return (mode,number_of_positions_planned,current_position_in_scan)
def start_scan(self):
interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0])
if interferometer_feedback_not_running == 1:
logger.error("Cannot start scan because feedback loop is not running or there is an interferometer error.")
raise RtLamniError("Cannot start scan because feedback loop is not running or there is an interferometer error.")
#here exception
(mode,number_of_positions_planned,current_position_in_scan)=self.get_scan_status()
if number_of_positions_planned == 0:
logger.error("Cannot start scan because no target positions are planned.")
raise RtLamniError("Cannot start scan because no target positions are planned.")
#hier exception
# start a point-by-point scan (for cont scan in flomni it would be "sa")
self.socket_put_and_receive("sd")
def start_readout(self):
readout = threading.Thread(target=self.read_positions_from_sampler)
readout.start()
def kickoff(self):
while not self._min_scan_buffer_reached:
time.sleep(0.001)
self.start_scan()
time.sleep(0.1)
self.start_readout()
def read_positions_from_sampler(self):
#this was for reading after the scan completed
number_of_samples_to_read = 1 #self.get_scan_status()[1] #number of valid samples, will be updated upon first data read
read_counter = 0
previous_point_in_scan = 0
average_stdeviations_x_st_fzp = 0
average_stdeviations_y_st_fzp = 0
average_lamni_angle = 0
mode,number_of_positions_planned,current_position_in_scan = self.get_scan_status()
#if not (mode==2 or mode==3):
# error
#while scan is running
while mode>0:
#logger.info(f"Current scan position {current_position_in_scan} out of {number_of_positions_planned}")
mode,number_of_positions_planned,current_position_in_scan = self.get_scan_status()
time.sleep(.01)
if (current_position_in_scan>5):
while current_position_in_scan > read_counter:
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
#logger.info(f"{return_table}")
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
read_counter = read_counter+1
average_stdeviations_x_st_fzp=average_stdeviations_x_st_fzp+float(return_table[5])
average_stdeviations_y_st_fzp=average_stdeviations_y_st_fzp+float(return_table[8])
average_lamni_angle = average_lamni_angle+float(return_table[19])
time.sleep(.05)
#read the last samples even though scan is finished already
while number_of_positions_planned > read_counter:
return_table = (self.socket_put_and_receive(f"r{read_counter}")).split(",")
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
#logger.info(f"{return_table}")
read_counter = read_counter+1
average_stdeviations_x_st_fzp=average_stdeviations_x_st_fzp+float(return_table[5])
average_stdeviations_y_st_fzp=average_stdeviations_y_st_fzp+float(return_table[8])
average_lamni_angle = average_lamni_angle+float(return_table[19])
logger.info(f"LamNI statistics: Average of all standard deviations: x {average_stdeviations_x_st_fzp/number_of_samples_to_read}, y {average_stdeviations_y_st_fzp/number_of_samples_to_read}, angle {average_lamni_angle/number_of_samples_to_read}.")
def feedback_status_angle_lamni(self) -> bool:
return_table = (self.socket_put_and_receive(f"J7")).split(",")
logger.debug(f"LamNI angle interferomter status {bool(return_table[0])}, position {float(return_table[1])}, signal {float(return_table[2])}")
return bool(return_table[0])
def feedback_enable_with_reset(self):
if not self.feedback_status_angle_lamni(self):
self.rt_feedback_disable_and_even_reset_lamni_angle_interferometer(self)
if not self.feedback_status_angle_lamni():
self.rt_feedback_disable_and_even_reset_lamni_angle_interferometer()
logger.info(f"LamNI resetting interferometer inclusive angular interferomter.")
else:
self.feedback_disable(self)
self.feedback_disable()
logger.info(f"LamNI resetting interferomter except angular interferometer which is already running.")
#set these as closed loop target position
#discuss: after this the current target position in lamni is 0, while the latest target position in ophyd might be different
#the same is after moving to a different scan region with the coarse stages. maybe issue a move command later to have them match?
#that is only possible with runnign feedback. or change user setpoint in a different way?
self.socket_put(f"pa0,0")
self.socket_put(f"pa1,0")
self.socket_put(f"pa2,0") #we set all three outputs of the traj. gen. although in LamNI case only 0,1 are used
self.clear_trajectory_generator(self)
####
#here umv lsamrot 0
####
#!lgalil_is_air_off_and_orchestra_enabled:
#Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller.
#exit with failure
self.socket_put(f"pa0,0")
self.get_axis_by_name('rtx').user_setpoint.setpoint = 0
self.socket_put(f"pa1,0")
self.get_axis_by_name('rty').user_setpoint.setpoint = 0
self.socket_put(f"pa2,0") #we set all three outputs of the traj. gen. although in LamNI case only 0,1 are used
self.clear_trajectory_generator()
self.get_device_manager().devices.lsamrot.obj.move(0, wait=True)
galil_controller_rt_status = self.get_device_manager().devices.lsamx.obj.controller.lgalil_is_air_off_and_orchestra_enabled()
if galil_controller_rt_status == 0:
logger.error("Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller.")
raise RtLamniError("Cannot enable feedback. The small rotation air is on and/or orchestra disabled by the motor controller.")
time.sleep(0.03)
#global _lsamx_center
#global _lsamy_center
#umv lsamx _lsamx_center
#umv lsamy _lsamy_center
#needs to be changed to configurable center psoition
self.get_device_manager().devices.lsamx.obj.move(8.866, wait=True)
self.get_device_manager().devices.lsamy.obj.move(10.18, wait=True)
self.socket_put("J1")
#set_lm rtx -50 50
#set_lm rty -50 50
_waitforfeedbackctr=0
#this is implemented as class and not function. why? RtLamniFeedbackRunning
interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0])
while interferometer_feedback_not_running == 1 and _waitforfeedbackctr<100:
time.sleep(0.01)
_waitforfeedbackctr = _waitforfeedbackctr + 1
interferometer_feedback_not_running = int((self.socket_put_and_receive("J2")).split(",")[0])
# while self.RtLamniFeedbackRunning _rt_status_feedback(self) == 1 && _waitforfeedbackctr<100)
# {
# sleep(0.01)
# _waitforfeedbackctr++
# #p _waitforfeedbackctr
# }
# motor_par("lsamx","disable",1)
# motor_par("lsamy","disable",1)
# motor_par("loptx","disable",1)
# motor_par("lopty","disable",1)
# motor_par("loptz","disable",1)
# rt_feedback_status
if interferometer_feedback_not_running == 1:
logger.error("Cannot start scan because feedback loop is not running or there is an interferometer error.")
raise RtLamniError("Cannot start scan because feedback loop is not running or there is an interferometer error.")
#}
time.sleep(0.01)
global ptychography_alignment_done
ptychography_alignment_done = 0
#ptychography_alignment_done = 0
@@ -355,6 +505,8 @@ class RtLamniMotor(Device, PositionerBase):
motor_is_moving = Cpt(
RtLamniMotorIsMoving, signal_name="motor_is_moving", kind="normal"
)
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
SUB_READBACK = "readback"
SUB_CONNECTION_CHANGE = "connection_change"
@@ -374,12 +526,15 @@ class RtLamniMotor(Device, PositionerBase):
port=3333,
sign=1,
socket_cls=SocketIO,
device_manager=None,
limits = None,
**kwargs,
):
self.axis_Id = axis_Id
self.sign = sign
self.controller = RtLamniController(socket=socket_cls(host=host, port=port))
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
self.device_manager = device_manager
self.tolerance = kwargs.pop("tolerance", 0.5)
super().__init__(
@@ -396,8 +551,31 @@ class RtLamniMotor(Device, PositionerBase):
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
)
self._update_connection_state()
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
# self.readback.subscribe(self._forward_readback, event_type=self.readback.SUB_VALUE)
if limits is not None:
assert len(limits) == 2
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())
@property
def low_limit(self):
return self.limits[0]
@property
def high_limit(self):
return self.limits[1]
def check_value(self, pos):
"""Check that the position is within the soft limits"""
low_limit, high_limit = self.limits
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
raise LimitError(f"position={pos} not within limits {self.limits}")
def _update_connection_state(self, **kwargs):
for walk in self.walk_signals():
@@ -440,7 +618,7 @@ class RtLamniMotor(Device, PositionerBase):
If motion fails other than timing out
"""
self._started_moving = False
timeout = kwargs.pop("timeout", 4)
timeout = kwargs.pop("timeout", 100)
status = super().move(position, timeout=timeout, **kwargs)
self.user_setpoint.put(position, wait=False)

View File

@@ -5,9 +5,9 @@ from typing import List
import numpy as np
from ophyd import Component as Cpt
from ophyd import Device, PositionerBase
from ophyd import Device, PositionerBase, Signal
from ophyd.status import wait as status_wait
from ophyd.utils import ReadOnlyError
from ophyd.utils import ReadOnlyError, LimitError
from ophyd_devices.smaract.smaract_controller import SmaractController
from ophyd_devices.smaract.smaract_errors import SmaractCommunicationError, SmaractError
from ophyd_devices.utils.socket import SocketIO, SocketSignal, raise_if_disconnected
@@ -93,7 +93,8 @@ class SmaractMotor(Device, PositionerBase):
# )
motor_is_moving = Cpt(SmaractMotorIsMoving, signal_name="motor_is_moving", kind="normal")
high_limit_travel = Cpt(Signal, value=0, kind="omitted")
low_limit_travel = Cpt(Signal, value=0, kind="omitted")
# all_axes_referenced = Cpt(
# SmaractAxesReferenced, signal_name="all_axes_referenced", kind="config"
# )
@@ -114,6 +115,7 @@ class SmaractMotor(Device, PositionerBase):
parent=None,
host="mpc2680.psi.ch",
port=8085,
limits=None,
sign=1,
socket_cls=SocketIO,
**kwargs,
@@ -138,6 +140,29 @@ class SmaractMotor(Device, PositionerBase):
self._update_connection_state, event_type=self.SUB_CONNECTION_CHANGE
)
self._update_connection_state()
if limits is not None:
assert len(limits) == 2
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())
@property
def low_limit(self):
return self.limits[0]
@property
def high_limit(self):
return self.limits[1]
def check_value(self, pos):
"""Check that the position is within the soft limits"""
low_limit, high_limit = self.limits
if low_limit < high_limit and not (low_limit <= pos <= high_limit):
raise LimitError(f"position={pos} not within limits {self.limits}")
def _update_connection_state(self, **kwargs):
for walk in self.walk_signals():