wip
This commit is contained in:
@@ -6,11 +6,12 @@ from bisect import bisect_right
|
||||
|
||||
import xraydb
|
||||
from bec_lib import bec_logger
|
||||
from ophyd_devices import CompareStatus
|
||||
|
||||
from ...devices.absorber import STATUS as ABS_STATUS
|
||||
from ...devices.eh_shutter import STATUS as EH_PH_STATUS
|
||||
from ...devices.ionization_chambers.ionization_chamber_enums import AmplifierEnable
|
||||
from ...devices.nidaq.nidaq_enums import NidaqState
|
||||
from ...devices.nidaq.nidaq_enums import EpicsMode, NidaqState
|
||||
from ...devices.op_shutter import STATUS as OP_PH_STATUS
|
||||
|
||||
logger = bec_logger.logger
|
||||
@@ -21,10 +22,12 @@ EMAX = 200
|
||||
MIN_RING_CURRENT = 5 # Minimum ring current to use auto-gain
|
||||
NOMINAL_RING_CURRENT = 400 # Nominal ring current of SLS2
|
||||
|
||||
TIMEOUT_PUT_PV = 5 # Timeout to set a PV
|
||||
|
||||
MONO_VELOCITY = 20 # Move velocity in deg/s
|
||||
TIMEOUT_MONO_PV = 5 # Timeout to set a PV on the mono
|
||||
TIMEOUT_MONO_MOVE = 30 # Timeout to finish a movement on the mono
|
||||
|
||||
LINKED_CHANNELS = {"ic0", "ic1"}
|
||||
AVAILABLE_GAINS = [1e6, 1e7, 5e7, 1e8, 1e9] # ascending order
|
||||
MIN_SIGNAL = 0.05 # Minimum signal to count as valid signal
|
||||
FULL_SCALE_V = 10.0 # NIDAQ AI full-scale range
|
||||
@@ -58,6 +61,10 @@ class AutoGain:
|
||||
"""Start the auto-gain sequence. Measure the signals of the specified
|
||||
amplifiers and set the gains accordingly. Makes sure there is actually beam available.
|
||||
|
||||
Note:
|
||||
If 'ic0' and 'ic1' are in the amplifier argument, the same gain will be assigned
|
||||
to both amplifiers to get the best glitch removal during processing later.
|
||||
|
||||
Args:
|
||||
element(str): Element which defines the energy at which the gain will be set, e.g. 'Cu'
|
||||
edge(str): Corresponding edge, e.g. 'L1'
|
||||
@@ -155,11 +162,16 @@ class AutoGain:
|
||||
init_vel = self.dev.mo1_bragg.velocity.get()
|
||||
|
||||
logger.info(f"Move mono to start of {emin} eV")
|
||||
self.dev.mo1_bragg.velocity.put(MONO_VELOCITY)
|
||||
status = CompareStatus(self.dev.mo1_bragg.velocity, MONO_VELOCITY)
|
||||
status.wait(TIMEOUT_PUT_PV)
|
||||
status = self.dev.mo1_bragg.move(emin)
|
||||
status.wait(TIMEOUT_MONO_MOVE)
|
||||
|
||||
# Set NIDAQ to max mode
|
||||
# TODO implement
|
||||
self.dev.nidaq.epics_mode.put(EpicsMode.MAX)
|
||||
status = CompareStatus(self.dev.nidaq.epics_mode, EpicsMode.MAX)
|
||||
status.wait(timeout=TIMEOUT_PUT_PV)
|
||||
|
||||
# Set gains to lowest gain
|
||||
for name, ch in active_channels.items():
|
||||
@@ -180,15 +192,14 @@ class AutoGain:
|
||||
raise AutoGainError("Ring current dropped to 0 mA right before measurement")
|
||||
|
||||
# Scan range, recording the peak NIDAQ signal per channel
|
||||
status = self.dev.mo1_bragg.velocity.put(MONO_VELOCITY)
|
||||
status.wait(TIMEOUT_MONO_PV)
|
||||
self.dev.mo1_bragg.move(emax).wait(timeout=TIMEOUT_MONO_MOVE)
|
||||
status.wait(TIMEOUT_MONO_MOVE)
|
||||
for name, ch in active_channels.items():
|
||||
data[name] = max(data[name], ch["signal"].get())
|
||||
|
||||
# Rest max values of NIDAQ signals
|
||||
# TODO implement
|
||||
self.dev.nidaq.epics_max_reset.put(True)
|
||||
time.sleep(0.1)
|
||||
|
||||
# Measure current ring current again
|
||||
ring_current_2 = self._get_ring_current()
|
||||
@@ -202,52 +213,83 @@ class AutoGain:
|
||||
|
||||
# Choose gain per channel based on the max signal recorded during the scan
|
||||
remeasure = False
|
||||
processed = set()
|
||||
|
||||
for name, ch in active_channels.items():
|
||||
raw_signal = data[name]
|
||||
logger.info(f"Raw signal for self.device {name} is {raw_signal} V")
|
||||
if comp_ring_current:
|
||||
raw_signal = raw_signal * NOMINAL_RING_CURRENT / ring_current
|
||||
logger.info(f"Compensate for ring current, new raw signal is {raw_signal} V")
|
||||
if raw_signal < MIN_SIGNAL:
|
||||
logger.info(f"Raw signal for self.device {name} is below {MIN_SIGNAL}")
|
||||
# Choose next gain to be 100x the current gain, or if this gain does not exist,
|
||||
# choose the next smaller one
|
||||
if ch["gain"] == AVAILABLE_GAINS[-1]:
|
||||
if name in processed:
|
||||
continue
|
||||
|
||||
# ic0/ic1 must always end up with the same gain -> treat whichever of
|
||||
# them are present (one or both) as a single group for this decision
|
||||
if name in LINKED_CHANNELS:
|
||||
group_names = [n for n in LINKED_CHANNELS if n in active_channels]
|
||||
else:
|
||||
group_names = [name]
|
||||
|
||||
group_signals = []
|
||||
for gname in group_names:
|
||||
gsig = data[gname]
|
||||
logger.info(f"Raw signal for device {gname} is {gsig} V")
|
||||
if comp_ring_current:
|
||||
gsig = gsig * NOMINAL_RING_CURRENT / ring_current
|
||||
logger.info(f"Compensate for ring current, new raw signal is {gsig} V")
|
||||
group_signals.append(gsig)
|
||||
|
||||
# worst case: the weakest signal drives "need more gain",
|
||||
# the strongest signal drives "don't saturate"
|
||||
raw_signal_min = min(group_signals)
|
||||
raw_signal_max = max(group_signals)
|
||||
gain_ref = ch[
|
||||
"gain"
|
||||
] # linked channels always share gain, so any member's value works
|
||||
label = "/".join(group_names)
|
||||
|
||||
if raw_signal_min < MIN_SIGNAL:
|
||||
logger.info(f"Raw signal for {label} is below {MIN_SIGNAL}")
|
||||
if gain_ref == AVAILABLE_GAINS[-1]:
|
||||
logger.warning(
|
||||
f"Amplifier of {name} at highest gain {ch['gain']} and still not"
|
||||
f"Amplifier of {label} at highest gain {gain_ref} and still not"
|
||||
+ f" measured signal above {MIN_SIGNAL}"
|
||||
)
|
||||
else:
|
||||
next_gain = AVAILABLE_GAINS[
|
||||
bisect_right(AVAILABLE_GAINS, ch["gain"] * 100) - 1
|
||||
bisect_right(AVAILABLE_GAINS, gain_ref * 100) - 1
|
||||
]
|
||||
ch["self.dev"].set_gain(next_gain)
|
||||
logger.info(
|
||||
f"Setting gain of self.device {name} to {next_gain:.0e} and remeasure"
|
||||
)
|
||||
for gname in group_names:
|
||||
active_channels[gname]["dev"].set_gain(next_gain)
|
||||
active_channels[gname]["gain"] = next_gain
|
||||
logger.info(f"Setting gain of {label} to {next_gain:.0e} and remeasure")
|
||||
remeasure = True
|
||||
else:
|
||||
gain = max(
|
||||
(
|
||||
g
|
||||
for g in AVAILABLE_GAINS
|
||||
if raw_signal / ch["gain"] * g <= FULL_SCALE_V * SAFETY_MARGIN
|
||||
if raw_signal_max / gain_ref * g <= FULL_SCALE_V * SAFETY_MARGIN
|
||||
),
|
||||
default=min(AVAILABLE_GAINS),
|
||||
)
|
||||
ch["self.dev"].set_gain(gain)
|
||||
logger.info(f"Calculated final gain for {name} of {gain:.0e}")
|
||||
for gname in group_names:
|
||||
active_channels[gname]["dev"].set_gain(gain)
|
||||
active_channels[gname]["gain"] = gain
|
||||
logger.info(f"Calculated final gain for {label} of {gain:.0e}")
|
||||
|
||||
processed.update(group_names)
|
||||
|
||||
# Wait for mono to return to start position
|
||||
status.wait(TIMEOUT_MONO_MOVE)
|
||||
|
||||
# Reset NIDAQ to mean mode
|
||||
# TODO implement
|
||||
self.dev.nidaq.epics_mode.put(EpicsMode.MEAN)
|
||||
status = CompareStatus(self.dev.nidaq.epics_mode, EpicsMode.MEAN)
|
||||
status.wait(timeout=TIMEOUT_PUT_PV)
|
||||
|
||||
# Wait for mono to move to initial position and reset velocity
|
||||
status = self.dev.mo1_bragg.move(init_pos)
|
||||
status.wait(TIMEOUT_MONO_MOVE)
|
||||
self.dev.mo1_bragg.velocity.put(init_vel)
|
||||
status = CompareStatus(self.dev.mo1_bragg.velocity, init_vel)
|
||||
status.wait(timeout=TIMEOUT_PUT_PV)
|
||||
|
||||
def _get_ring_current(self) -> float:
|
||||
ring_current = 0
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import builtins
|
||||
import re
|
||||
import threading
|
||||
from pathlib import Path
|
||||
from typing import Literal, cast
|
||||
|
||||
@@ -10,6 +12,7 @@ from xrt.backends.raycing.physconsts import AVOGADRO, CHeVcm
|
||||
|
||||
from . import parameters as bl
|
||||
from .beamline import get_beamline_id
|
||||
from .motion_worker import MotionWorker
|
||||
from .types import BeamlineId, ConfigDict, DataDict, SurfaceDict
|
||||
|
||||
H = 6.62606957e-34
|
||||
@@ -22,18 +25,65 @@ OFFSET_FILE_X10DA = Path(__file__).with_name("x10da_offsets.yaml")
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
"""
|
||||
The idea is to move the core logic of digital_twin to this file. Only keep the gui elements in the widget.
|
||||
This way, the scheduler widget can access digital twin without loading the GUI (GUI is still needed for the item creation, but not the item execution)
|
||||
Scheduler will extract assistant inputs (get_assistant_config) during item creation
|
||||
Scheduler will use digital_twin.calculate_positons to calculate positions and digital_twin.move_all to move the motors
|
||||
"""
|
||||
|
||||
class _ExclusiveGroupManager:
|
||||
"""Coordinates one exclusive group: only motors from the same
|
||||
subgroup may be active at once; a different subgroup must wait until
|
||||
the active one fully finishes."""
|
||||
|
||||
def __init__(self):
|
||||
self._cond = threading.Condition()
|
||||
self._active_subgroup = None
|
||||
self._active_count = 0
|
||||
|
||||
def acquire(self, subgroup_id):
|
||||
with self._cond:
|
||||
while self._active_subgroup is not None and self._active_subgroup != subgroup_id:
|
||||
self._cond.wait()
|
||||
self._active_subgroup = subgroup_id
|
||||
self._active_count += 1
|
||||
|
||||
def release(self):
|
||||
with self._cond:
|
||||
self._active_count -= 1
|
||||
if self._active_count == 0:
|
||||
self._active_subgroup = None
|
||||
self._cond.notify_all()
|
||||
|
||||
|
||||
# Each entry is one exclusive group, made of subgroups. Motors in the same
|
||||
# subgroup may move concurrently; motors in different subgroups of the
|
||||
# same group may not. Motors not listed here move completely freely.
|
||||
# Example from below: Each listed cm_* motor needs to move alone, but
|
||||
# sldi_gapx and sldi_gapy can move together. sldi_gapx can move together
|
||||
# with ot_try
|
||||
EXCLUSIVE_GROUPS: list[list[list[str]]] = [
|
||||
[["sldi_gapx", "sldi_gapy"], ["sldi_centerx", "sldi_centery"]],
|
||||
[["cm_trx"], ["cm_roty"], ["cm_try"], ["cm_rotx"], ["cm_rotz"]],
|
||||
[["fm_trx"], ["fm_roty"], ["fm_try"], ["fm_rotx"], ["fm_rotz"]],
|
||||
[["sl1_gapx", "sl1_gapy"], ["sl1_centerx", "sl1_centery"]],
|
||||
[["sl2_gapx", "sl2_gapy"], ["sl2_centerx", "sl2_centery"]],
|
||||
[["ot_try"], ["ot_rotx"]],
|
||||
]
|
||||
|
||||
|
||||
class DigitalTwinCoreError(Exception):
|
||||
"""DigitalTwinCore specific error"""
|
||||
|
||||
|
||||
class DigitalTwinCore:
|
||||
|
||||
def __init__(self):
|
||||
logger.info("This is the digital twin from the ipython client!")
|
||||
dev = builtins.__dict__.get("dev")
|
||||
bec = builtins.__dict__.get("bec")
|
||||
if dev is None:
|
||||
raise DigitalTwinCoreError("Did not get dev")
|
||||
if bec is None:
|
||||
raise DigitalTwinCoreError("Did not get bec")
|
||||
self.dev = dev
|
||||
self.bec = bec
|
||||
|
||||
self.beamline = get_beamline_id()
|
||||
self.offset_file = Path()
|
||||
match self.beamline:
|
||||
@@ -44,10 +94,69 @@ class DigitalTwinCore:
|
||||
self.offsets = {}
|
||||
self.load_offsets()
|
||||
|
||||
def _exclusive_group_key(self, motor: str):
|
||||
"""Return (group_index, subgroup_index) for `motor`, or (None, None)
|
||||
if it isn't part of any exclusive group and can move freely."""
|
||||
for gi, group in enumerate(EXCLUSIVE_GROUPS):
|
||||
for si, subgroup in enumerate(group):
|
||||
if motor in subgroup:
|
||||
return gi, si
|
||||
return None, None
|
||||
|
||||
def move_with_config(self, config):
|
||||
"""
|
||||
Move all motors in `config` to their target positions, all starting at
|
||||
once. Motors are free to move concurrently unless they belong to the
|
||||
same exclusive group (see `EXCLUSIVE_GROUPS`) but different subgroups,
|
||||
in which case one subgroup must fully finish before another starts.
|
||||
|
||||
Returns:
|
||||
dict: {"success": True, "moved": [...]} if every motor completed
|
||||
without error, else {"success": False, "moved": [...],
|
||||
"failures": {motor: error}}.
|
||||
"""
|
||||
positions = self.calc_positions(self.beamline, config)
|
||||
positions = self.apply_offsets(positions, nested_config=True)
|
||||
logger.info(f"Would now move to these positions: {positions}")
|
||||
|
||||
managers: dict[int, _ExclusiveGroupManager] = {}
|
||||
for motor in positions:
|
||||
gi, _ = self._exclusive_group_key(motor)
|
||||
if gi is not None:
|
||||
managers.setdefault(gi, _ExclusiveGroupManager())
|
||||
|
||||
moved: list[str] = []
|
||||
failures: dict[str, Exception | str] = {}
|
||||
results_lock = threading.Lock()
|
||||
|
||||
def run_motor(motor: str, target: float):
|
||||
gi, si = self._exclusive_group_key(motor)
|
||||
manager = managers.get(gi) if gi is not None else None
|
||||
if manager:
|
||||
manager.acquire(si)
|
||||
try:
|
||||
worker = MotionWorker(self.beamline, self.dev, motor, target)
|
||||
worker.run() # blocks this thread until finished or errored
|
||||
with results_lock:
|
||||
if worker.last_exception is not None:
|
||||
failures[motor] = worker.last_exception
|
||||
else:
|
||||
moved.append(motor)
|
||||
finally:
|
||||
if manager:
|
||||
manager.release()
|
||||
|
||||
threads = [
|
||||
threading.Thread(target=run_motor, args=(motor, pos["value"]), daemon=True)
|
||||
for motor, pos in positions.items()
|
||||
]
|
||||
for t in threads:
|
||||
t.start()
|
||||
for t in threads:
|
||||
t.join()
|
||||
|
||||
if failures:
|
||||
return {"success": False, "moved": moved, "failures": failures}
|
||||
return {"success": True, "moved": moved}
|
||||
|
||||
def load_offsets(self):
|
||||
if self.offsets == {}:
|
||||
@@ -255,7 +364,7 @@ class DigitalTwinCore:
|
||||
|
||||
## Focusing Mirror
|
||||
p = bl.fm.center[1]
|
||||
q = cfg["smpl"] - bl.fm.center[1]
|
||||
q = (cfg["ot_es1_trz"] + cfg["es1man_trz"]) - bl.fm.center[1]
|
||||
f = (p * q) / (p + q) # focal length
|
||||
|
||||
# Bender radius
|
||||
@@ -350,9 +459,22 @@ class DigitalTwinCore:
|
||||
pos["ot_rotx"] = {"value": ot_pitch * 1e3}
|
||||
|
||||
# TRZ ES1
|
||||
ot_es1_trz = cfg["smpl"]
|
||||
ot_es1_trz = cfg["ot_es1_trz"]
|
||||
pos["ot_es1_trz"] = {"value": ot_es1_trz}
|
||||
|
||||
# Sample manipulator TRZ
|
||||
es1man_trz = cfg["es1man_trz"]
|
||||
if es1man_trz is None:
|
||||
raise ValueError("es1man_trz is None")
|
||||
pos["es1man_trz"] = {"value": es1man_trz}
|
||||
|
||||
# TRZ ES2
|
||||
smpl_to_xrd = cfg["smpl_to_xrd"]
|
||||
if smpl_to_xrd is None:
|
||||
raise ValueError("smpl_to_xrd is None")
|
||||
ot_es2_trz = smpl_to_xrd + ot_es1_trz + es1man_trz - 32
|
||||
pos["ot_es2_trz"] = {"value": ot_es2_trz}
|
||||
|
||||
# ES0 exit window
|
||||
pos["es0wi_try"] = {
|
||||
"value": 5
|
||||
@@ -930,6 +1052,7 @@ class DigitalTwinCore:
|
||||
beam["y"].append(bl.sourceHeight)
|
||||
beam["x"].append(bl.cm.center[1]) # CM
|
||||
beam["y"].append(bl.sourceHeight)
|
||||
smpl = cfg["ot_es1_trz"] + cfg["es1man_trz"]
|
||||
if cfg["mo1_mode"] == "Monochromatic":
|
||||
diag = bl.mo1.xtalGap[0] / np.sin(cfg["mo1_bragg"]) # Calculations for Mono
|
||||
dy = diag * np.sin(2 * (cfg["cm_pitch"] + cfg["mo1_bragg"]))
|
||||
@@ -951,23 +1074,23 @@ class DigitalTwinCore:
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz)
|
||||
+ dy
|
||||
)
|
||||
beam["x"].append(cfg["smpl"]) # Experiment
|
||||
beam["x"].append(smpl) # Experiment
|
||||
beam["y"].append(
|
||||
bl.sourceHeight
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1] - dz)
|
||||
+ dy
|
||||
+ np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1])
|
||||
+ np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (smpl - bl.fm.center[1])
|
||||
)
|
||||
elif cfg["mo1_mode"] == "Pinkbeam":
|
||||
beam["x"].append(bl.fm.center[1]) # FM
|
||||
beam["y"].append(
|
||||
bl.sourceHeight + np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1])
|
||||
)
|
||||
beam["x"].append(cfg["smpl"]) # Experiment
|
||||
beam["x"].append(smpl) # Experiment
|
||||
beam["y"].append(
|
||||
bl.sourceHeight
|
||||
+ np.tan(2 * cfg["cm_pitch"]) * (bl.fm.center[1] - bl.cm.center[1])
|
||||
+ np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (cfg["smpl"] - bl.fm.center[1])
|
||||
+ np.tan(2 * (cfg["cm_pitch"] - cfg["fm_rotx"])) * (smpl - bl.fm.center[1])
|
||||
)
|
||||
|
||||
return beam
|
||||
|
||||
@@ -0,0 +1,269 @@
|
||||
"""Move an axis through BEC"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import Callable
|
||||
|
||||
from bec_lib import bec_logger
|
||||
|
||||
# pylint: disable=E0402
|
||||
from ....devices.absorber import STATUS as ABS_STATUS
|
||||
from .types import BeamlineId
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class MotionWorker:
|
||||
"""
|
||||
Executes motion on the specified motor and includes some safety during
|
||||
motion for certain motors. Pure Python / Qt-independent: reports
|
||||
progress via plain callables instead of Qt signals, so it can run on
|
||||
any thread (or be unit-tested without a QApplication).
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
beamline: BeamlineId,
|
||||
dev,
|
||||
motor,
|
||||
target_pos: float,
|
||||
on_position_changed: Callable[[float], None] | None = None,
|
||||
on_error: Callable[[], None] | None = None,
|
||||
on_finished: Callable[[], None] | None = None,
|
||||
):
|
||||
self.beamline = beamline
|
||||
self.dev = dev
|
||||
self.motor = motor
|
||||
self._target = target_pos
|
||||
self._stop_flag = threading.Event()
|
||||
self.on_position_changed = on_position_changed
|
||||
self.on_error = on_error
|
||||
self.on_finished = on_finished
|
||||
self.last_exception: Exception | None = None
|
||||
|
||||
def _emit_position_changed(self, pos: float):
|
||||
if self.on_position_changed is not None:
|
||||
self.on_position_changed(pos)
|
||||
|
||||
def _emit_error(self):
|
||||
if self.on_error is not None:
|
||||
self.on_error()
|
||||
|
||||
def _emit_finished(self):
|
||||
if self.on_finished is not None:
|
||||
self.on_finished()
|
||||
|
||||
def stop(self):
|
||||
"""Sets the stop flag"""
|
||||
self._stop_flag.set()
|
||||
|
||||
def run(self):
|
||||
"""Prepares the movement based on the axis (motor)"""
|
||||
match self.motor:
|
||||
case (
|
||||
"sldi_gapx"
|
||||
| "sldi_gapy"
|
||||
| "sldi_centerx"
|
||||
| "sldi_centery"
|
||||
| "sl1_gapx"
|
||||
| "sl1_gapy"
|
||||
| "sl1_centerx"
|
||||
| "sl1_centery"
|
||||
| "bm1_try"
|
||||
| "sl2_gapx"
|
||||
| "sl2_gapy"
|
||||
| "sl2_centerx"
|
||||
| "sl2_centery"
|
||||
| "bm2_try"
|
||||
| "es0wi_try"
|
||||
| "es1_try"
|
||||
| "es2_try"
|
||||
| "ot_try"
|
||||
| "ot_rotx"
|
||||
| "ot_es1_trz"
|
||||
| "ot_es2_trz"
|
||||
| "es1ic0_try"
|
||||
| "es1ic1_try"
|
||||
| "es1ic2_try"
|
||||
):
|
||||
self.motion()
|
||||
case "cm_trx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[{"device": self.dev["cm_roty"], "abs_tol": 0.05}],
|
||||
)
|
||||
case "cm_roty":
|
||||
self.motion(
|
||||
abs_closed=True, surveyed_axes=[{"device": self.dev["cm_trx"], "abs_tol": 0.05}]
|
||||
)
|
||||
case "cm_try":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["cm_rotx"], "abs_tol": 0.05},
|
||||
{"device": self.dev["cm_rotz"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "cm_rotx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["cm_try"], "abs_tol": 0.05},
|
||||
{"device": self.dev["cm_rotz"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "cm_rotz":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["cm_try"], "abs_tol": 0.05},
|
||||
{"device": self.dev["cm_rotx"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "cm_bnd":
|
||||
if self.beamline == "x01da":
|
||||
p1 = (
|
||||
1 / (self.dev.cm_bnd_radius.read()["cm_bnd_radius"]["value"] * 1e3) + 0.0284
|
||||
) / 2e-6
|
||||
p2 = (1 / (self._target * 1e3) + 0.0284) / 2e-6
|
||||
else:
|
||||
p1 = 541900 / self.dev.cm_bnd_radius.read()["cm_bnd_radius"]["value"] - 32570
|
||||
p2 = 541900 / self._target - 32570
|
||||
self._target = p2 - p1
|
||||
self.motion(relative=True, rb={"device": self.dev["cm_bnd_radius"]})
|
||||
case "mo1_try" | "mo1_trx" | "mo1_roty":
|
||||
self.motion(abs_closed=True)
|
||||
case "mo1_bragg_angle":
|
||||
if self.beamline == "x01da":
|
||||
self.motion()
|
||||
else: # x10da needs to move goniometer
|
||||
self.motion(alias="mo1_rotx")
|
||||
case "fm_trx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[{"device": self.dev["fm_roty"], "abs_tol": 0.05}],
|
||||
)
|
||||
case "fm_roty":
|
||||
self.motion(
|
||||
abs_closed=True, surveyed_axes=[{"device": self.dev["fm_trx"], "abs_tol": 0.05}]
|
||||
)
|
||||
case "fm_try":
|
||||
if self.beamline == "x01da":
|
||||
abs_tol = 0.05
|
||||
else: # superxas mirror less stable thus needs higher tolerance
|
||||
abs_tol = 0.2
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["fm_rotx"], "abs_tol": abs_tol},
|
||||
{"device": self.dev["fm_rotz"], "abs_tol": abs_tol},
|
||||
],
|
||||
)
|
||||
case "fm_rotx":
|
||||
if self.beamline == "x01da":
|
||||
abs_tol = 0.05
|
||||
else: # superxas mirror less stable thus needs higher tolerance
|
||||
abs_tol = 0.2
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["fm_try"], "abs_tol": abs_tol},
|
||||
{"device": self.dev["fm_rotz"], "abs_tol": abs_tol},
|
||||
],
|
||||
)
|
||||
case "fm_rotz":
|
||||
if self.beamline == "x01da":
|
||||
abs_tol = 0.05
|
||||
else: # superxas mirror less stable thus needs higher tolerance
|
||||
abs_tol = 0.2
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["fm_try"], "abs_tol": abs_tol},
|
||||
{"device": self.dev["fm_rotx"], "abs_tol": abs_tol},
|
||||
],
|
||||
)
|
||||
case "fm_bnd":
|
||||
if self.beamline == "x01da":
|
||||
p1 = (
|
||||
1 / (self.dev.fm_bnd_radius.read()["fm_bnd_radius"]["value"] * 1e3)
|
||||
+ 4.28e-5
|
||||
) / 1.84e-9
|
||||
p2 = (1 / (self._target * 1e3) + 4.28e-5) / 1.84e-9
|
||||
else:
|
||||
p1 = (
|
||||
593088.7 / self.dev.fm_bnd_radius.read()["fm_bnd_radius"]["value"]
|
||||
+ 26124.41
|
||||
)
|
||||
p2 = 593088.7 / self._target + 26124.41
|
||||
self._target = p2 - p1
|
||||
self.motion(relative=True, rb={"device": self.dev["fm_bnd_radius"]})
|
||||
case _:
|
||||
logger.warning(f"Motor {self.motor} not integrated in digital twin!")
|
||||
|
||||
def motion(
|
||||
self,
|
||||
abs_closed: bool = False,
|
||||
relative: bool = False,
|
||||
rb=None,
|
||||
surveyed_axes=None,
|
||||
alias=None,
|
||||
):
|
||||
"""
|
||||
Moves an axis while surverying a set of axes (if set).
|
||||
Example surveyed_axes:
|
||||
[{'device': bec_device_object, 'abs_tol': 0.1},]
|
||||
|
||||
Args:
|
||||
surveyed_axes (list): List of dictionaries of devices
|
||||
"""
|
||||
logger.info(f"Move axis {self.motor} to target {self._target}, move_relative={relative}")
|
||||
try:
|
||||
if alias:
|
||||
self.motor = alias
|
||||
if abs_closed:
|
||||
if self.dev.abs.status.get() == ABS_STATUS.OPEN:
|
||||
status = self.dev.abs.close()
|
||||
status.wait(timeout=5)
|
||||
if surveyed_axes is not None:
|
||||
for surv_ax in surveyed_axes:
|
||||
surv_ax["name"] = surv_ax["device"].dotted_name
|
||||
surv_ax["old_value"] = surv_ax["device"].read(cached=True)[surv_ax["name"]][
|
||||
"value"
|
||||
]
|
||||
if rb is not None:
|
||||
rb["name"] = rb["device"].dotted_name
|
||||
status = self.dev[self.motor].move(self._target, relative=relative)
|
||||
last_check = time.time()
|
||||
update_interval = 0.1
|
||||
while status.status == "RUNNING":
|
||||
now = time.time()
|
||||
if time.time() - last_check < update_interval:
|
||||
time.sleep(0.01)
|
||||
last_check = now
|
||||
if self._stop_flag.is_set():
|
||||
self.dev[self.motor].stop()
|
||||
self._stop_flag.clear()
|
||||
if rb is not None:
|
||||
self._emit_position_changed(rb["device"].read(cached=True)[rb["name"]]["value"])
|
||||
else:
|
||||
self._emit_position_changed(
|
||||
self.dev[self.motor].read(cached=True)[self.motor]["value"]
|
||||
)
|
||||
if surveyed_axes is not None:
|
||||
for surv_ax in surveyed_axes:
|
||||
fb = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"]
|
||||
if abs(fb - surv_ax["old_value"]) > surv_ax["abs_tol"]:
|
||||
self.dev[self.motor].stop()
|
||||
self.last_exception = RuntimeError(
|
||||
f"{surv_ax['name']} deviated beyond tolerance {surv_ax['abs_tol']} during {self.motor} move"
|
||||
)
|
||||
self._emit_error()
|
||||
self._emit_finished()
|
||||
return
|
||||
self._emit_finished()
|
||||
except Exception as e:
|
||||
logger.error(f"Error during movement of {self.motor}: {e}")
|
||||
self.last_exception = e
|
||||
self._emit_error()
|
||||
self._emit_finished()
|
||||
@@ -32,7 +32,9 @@ class ConfigDict(TypedDict):
|
||||
fm_trx (float): FM translation x.
|
||||
fm_qy (float): FM qy value.
|
||||
fm_gain_height (int): FM gain height.
|
||||
smpl (float): Sample value.
|
||||
ot_es1_trz (float): Position of ES1 table.
|
||||
es1man_trz (float): Position of Sample Manipulator TRZ
|
||||
smpl_to_xrd (float): Sample to XRD detector distance
|
||||
"""
|
||||
|
||||
energy: float
|
||||
@@ -49,7 +51,9 @@ class ConfigDict(TypedDict):
|
||||
fm_trx: float
|
||||
fm_qy: None | float
|
||||
fm_gain_height: int
|
||||
smpl: float
|
||||
ot_es1_trz: float
|
||||
es1man_trz: float
|
||||
smpl_to_xrd: None | float
|
||||
|
||||
|
||||
class DataDict(TypedDict):
|
||||
|
||||
@@ -42,7 +42,6 @@ from .panels.input_panel import InputPanel
|
||||
from .panels.mover_panel import MoverPanel
|
||||
from .panels.plots import SideviewPlot, SurfacePlots
|
||||
from .panels.settings_panel import SettingsPanel
|
||||
from .widgets.qt_widgets import ComboBox, InputNumberField
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -133,11 +132,11 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
self.input.fm_rotx.value_changed_connect(self.calc_assistant)
|
||||
self.input.fm_focx.value_changed_connect(self.calc_assistant)
|
||||
self.input.fm_focy.value_changed_connect(self.calc_assistant)
|
||||
match self.input.smpl:
|
||||
case InputNumberField():
|
||||
self.input.smpl.value_changed_connect(self.calc_assistant)
|
||||
case ComboBox():
|
||||
self.input.smpl.activated_connect(self.calc_assistant)
|
||||
if self.beamline == "x01da":
|
||||
self.input.ot_es1_trz.value_changed_connect(self.calc_assistant)
|
||||
else:
|
||||
self.input.table.activated_connect(self.calc_assistant)
|
||||
self.input.es1man_trz.value_changed_connect(self.calc_assistant)
|
||||
|
||||
self.input.adapt_reality.clicked_connect(self.adapt_reality)
|
||||
self.settings.load_offsets.clicked_connect(self.load_offsets)
|
||||
@@ -216,6 +215,7 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
"fm_rotz",
|
||||
"bm2_try",
|
||||
"es0wi_try",
|
||||
"es1man_trz",
|
||||
]
|
||||
if self.beamline == "x01da": # X01DA specific devices
|
||||
devices.extend(
|
||||
@@ -226,6 +226,7 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
"sl2_gapy",
|
||||
"ot_try",
|
||||
"ot_es1_trz",
|
||||
"ot_es2_trz",
|
||||
]
|
||||
)
|
||||
if self.beamline == "x10da": # X10DA specific devices
|
||||
@@ -344,7 +345,9 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
self.calc_fm_reflectivity()
|
||||
self.calc_cm_fm_harm_suppr()
|
||||
self.calc_fm_ideal_pitch()
|
||||
case "smpl":
|
||||
case "ot_es1_trz":
|
||||
self.calc_fm_ideal_pitch()
|
||||
case "es1man_trz":
|
||||
self.calc_fm_ideal_pitch()
|
||||
self.calc_positions()
|
||||
self.calc_assistant_sideview()
|
||||
@@ -380,12 +383,15 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
assert cm_trx is not None, f"No cm_trx found for given stripe {cm_stripe}!"
|
||||
assert fm_trx is not None, f"No fm_trx found for given stripe {fm_stripe}!"
|
||||
|
||||
match self.input.smpl:
|
||||
case InputNumberField():
|
||||
smpl = self.input.smpl.value()
|
||||
case ComboBox():
|
||||
table = self.input.smpl.currentText()
|
||||
smpl = self.core.table_to_smpl_pos(table)
|
||||
if self.beamline == "x01da":
|
||||
ot_es1_trz = self.input.ot_es1_trz.value()
|
||||
smpl_to_xrd = self.input.smpl_to_xrd.value()
|
||||
else:
|
||||
table = self.input.table.currentText()
|
||||
ot_es1_trz = self.core.table_to_smpl_pos(table)
|
||||
smpl_to_xrd = None
|
||||
|
||||
es1man_trz = self.input.es1man_trz.value()
|
||||
|
||||
config: ConfigDict = {
|
||||
"energy": self.input.energy.value(),
|
||||
@@ -402,7 +408,9 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
"fm_trx": fm_trx,
|
||||
"fm_qy": fm_qy,
|
||||
"fm_gain_height": 1,
|
||||
"smpl": smpl,
|
||||
"ot_es1_trz": ot_es1_trz,
|
||||
"es1man_trz": es1man_trz,
|
||||
"smpl_to_xrd": smpl_to_xrd,
|
||||
}
|
||||
|
||||
# Apply offsets
|
||||
@@ -441,13 +449,16 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
fm_stripe = self.core.fm_trx_to_stripe(-fm_trx)
|
||||
fm_rotx = self.dev.fm_rotx.read(cached=True)["fm_rotx"]["value"]
|
||||
fm_rotx_real = 2 * cm_pitch - fm_rotx
|
||||
es1man_trz = self.dev.es1man_trz.read(cached=True)["es1man_trz"]["value"]
|
||||
|
||||
match self.input.smpl:
|
||||
case InputNumberField():
|
||||
smpl = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"]
|
||||
case ComboBox():
|
||||
table = self.input.smpl.currentText()
|
||||
smpl = self.core.table_to_smpl_pos(table)
|
||||
if self.beamline == "x01da":
|
||||
ot_es1_trz = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"]
|
||||
ot_es2_trz = self.dev.ot_es2_trz.read(cached=True)["ot_es2_trz"]["value"]
|
||||
smpl_to_xrd = ot_es2_trz - ot_es1_trz - es1man_trz + 32
|
||||
else:
|
||||
table = self.input.table.currentText()
|
||||
ot_es1_trz = self.core.table_to_smpl_pos(table)
|
||||
smpl_to_xrd = None
|
||||
|
||||
raw = { # Config in SI units!
|
||||
"energy": mo1_bragg["mo1_bragg"]["value"],
|
||||
@@ -464,7 +475,9 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
"fm_trx": fm_trx,
|
||||
"fm_qy": None,
|
||||
"fm_gain_height": 1,
|
||||
"smpl": smpl,
|
||||
"ot_es1_trz": ot_es1_trz,
|
||||
"es1man_trz": es1man_trz,
|
||||
"smpl_to_xrd": smpl_to_xrd,
|
||||
}
|
||||
config = cast(ConfigDict, raw)
|
||||
# logger.info(f'Config created: {config}')
|
||||
@@ -517,7 +530,9 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
if self.beamline == "x01da":
|
||||
self.mover.ot_try.set_feedback(self.dev.ot_try.read(cached=True)["ot_try"]["value"])
|
||||
self.mover.ot_rotx.set_feedback(self.dev.ot_rotx.read(cached=True)["ot_rotx"]["value"])
|
||||
self.mover.ot_es1_trz.set_feedback(smpl)
|
||||
self.mover.ot_es1_trz.set_feedback(ot_es1_trz)
|
||||
self.mover.ot_es2_trz.set_feedback(ot_es2_trz)
|
||||
self.mover.es1man_trz.set_feedback(es1man_trz)
|
||||
self.mover.es0wi_try.set_feedback(
|
||||
self.dev.es0wi_try.read(cached=True)["es0wi_try"]["value"]
|
||||
)
|
||||
@@ -552,8 +567,14 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
pos["fm_trx"] = self.dev.fm_trx.read(cached=True)["fm_trx"]["value"]
|
||||
pos["fm_rotx"] = self.dev.fm_rotx.read(cached=True)["fm_rotx"]["value"]
|
||||
pos["fm_bnd_radius"] = self.dev.fm_bnd_radius.read(cached=True)["fm_bnd_radius"]["value"]
|
||||
pos["es1man_trz"] = self.dev.es1man_trz.read(cached=True)["es1man_trz"]["value"]
|
||||
if self.beamline == "x01da":
|
||||
pos["ot_es1_trz"] = self.dev.ot_es1_trz.read(cached=True)["ot_es1_trz"]["value"]
|
||||
pos["ot_es2_trz"] = self.dev.ot_es2_trz.read(cached=True)["ot_es2_trz"]["value"]
|
||||
pos["smpl_to_xrd"] = pos["ot_es2_trz"] - pos["ot_es1_trz"] - pos["es1man_trz"] + 32
|
||||
else:
|
||||
table = self.ask_table_selection(self.input.table.currentText())
|
||||
pos["ot_es1_trz"] = self.core.table_to_smpl_pos(table)
|
||||
|
||||
# Removing offsets
|
||||
pos = self.core.remove_offsets(pos)
|
||||
@@ -577,14 +598,14 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
fm_rotx_real = 2 * pos["cm_rotx"] - pos["fm_rotx"]
|
||||
self.input.fm_rotx.set_number(fm_rotx_real)
|
||||
|
||||
match self.input.smpl:
|
||||
case InputNumberField():
|
||||
smpl = pos["ot_es1_trz"]
|
||||
self.input.smpl.set_number(pos["ot_es1_trz"])
|
||||
case ComboBox():
|
||||
table = self.ask_table_selection(self.input.smpl.currentText())
|
||||
smpl = self.core.table_to_smpl_pos(table)
|
||||
self.input.smpl.set_current_text(table)
|
||||
if self.beamline == "x01da":
|
||||
self.input.ot_es1_trz.set_number(pos["ot_es1_trz"])
|
||||
self.input.smpl_to_xrd.set_number(pos["smpl_to_xrd"])
|
||||
else:
|
||||
table = self.ask_table_selection(self.input.table.currentText())
|
||||
self.input.table.set_current_text(table)
|
||||
|
||||
smpl = pos["ot_es1_trz"] + pos["es1man_trz"]
|
||||
|
||||
fm_focx, fm_focy = self.core.calc_beamsize(
|
||||
h_acc, v_acc, fm_stripe, -fm_rotx_real * 1e-3, pos["fm_bnd_radius"] * 1e6, smpl
|
||||
@@ -866,7 +887,9 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
self.mover.ot_try.set_target(out["ot_try"]["value"])
|
||||
self.mover.ot_rotx.set_target(out["ot_rotx"]["value"])
|
||||
self.mover.ot_es1_trz.set_target(out["ot_es1_trz"]["value"])
|
||||
self.mover.ot_es2_trz.set_target(out["ot_es2_trz"]["value"])
|
||||
self.mover.es0wi_try.set_target(out["es0wi_try"]["value"])
|
||||
self.mover.es1man_trz.set_target(out["es1man_trz"]["value"])
|
||||
if self.beamline == "x10da":
|
||||
self.mover.es1_try.set_target(out["es1_try"]["value"])
|
||||
self.mover.es1ic0_try.set_target(out["es1ic0_try"]["value"])
|
||||
@@ -918,12 +941,14 @@ class DigitalTwin(BECWidget, QWidget):
|
||||
Literal["Defocused", "Focused", "Manual"], self.input.fm_focus.currentText()
|
||||
)
|
||||
fm_stripe = self.input.fm_stripe.currentText()
|
||||
match self.input.smpl:
|
||||
case InputNumberField():
|
||||
smpl = self.input.smpl.value()
|
||||
case ComboBox():
|
||||
table = self.input.smpl.currentText()
|
||||
smpl = self.core.table_to_smpl_pos(table)
|
||||
es1man_trz = self.input.es1man_trz.value()
|
||||
if self.beamline == "x01da":
|
||||
ot_es1_trz = self.input.ot_es1_trz.value()
|
||||
else:
|
||||
table = self.input.table.currentText()
|
||||
ot_es1_trz = self.core.table_to_smpl_pos(table)
|
||||
smpl = ot_es1_trz + es1man_trz
|
||||
|
||||
sldi_hacc = self.input.sldi_hacc.value() * 1e-3
|
||||
sldi_vacc = self.input.sldi_vacc.value() * 1e-3
|
||||
fm_focx = self.input.fm_focx.value()
|
||||
|
||||
@@ -173,7 +173,45 @@ class InputPanel(QWidget):
|
||||
|
||||
# Sample
|
||||
self.cm_fm_harm_suppr = NumberIndicator("Total Suppression Factor at x eV", "", decimals=0)
|
||||
self.smpl = self._create_smpl(beamline)
|
||||
|
||||
self.es1man_trz = InputNumberField(
|
||||
"es1man_trz",
|
||||
"Sample Manipulator TRZ",
|
||||
unit="mm",
|
||||
init=0,
|
||||
decimals=1,
|
||||
single_step=1,
|
||||
ll=-60,
|
||||
hl=60,
|
||||
)
|
||||
|
||||
if beamline == BeamlineId.X01DA:
|
||||
self.ot_es1_trz = InputNumberField(
|
||||
"ot_es1_trz",
|
||||
"ES1 Position",
|
||||
unit="mm",
|
||||
init=23511,
|
||||
decimals=0,
|
||||
single_step=100,
|
||||
ll=23000,
|
||||
hl=30000,
|
||||
)
|
||||
self.smpl_to_xrd = InputNumberField(
|
||||
"smpl_to_xrd",
|
||||
"Sample to XRD Det",
|
||||
unit="mm",
|
||||
init=320,
|
||||
decimals=0,
|
||||
single_step=10,
|
||||
ll=320,
|
||||
hl=6000,
|
||||
)
|
||||
self.smpl_group = Group(
|
||||
"Sample Position", [self.ot_es1_trz, self.es1man_trz, self.smpl_to_xrd]
|
||||
)
|
||||
else:
|
||||
self.table = ComboBox("table", "Sample Position", ["ES1", "ES2"])
|
||||
self.smpl_group = Group("Sample Position", [self.ot_es1_trz, self.es1man_trz])
|
||||
|
||||
# Assemble complete assistant group
|
||||
self.input_group = Group(
|
||||
@@ -186,25 +224,9 @@ class InputPanel(QWidget):
|
||||
self.mo1_ass_group,
|
||||
self.fm_ass_group,
|
||||
self.cm_fm_harm_suppr,
|
||||
self.smpl,
|
||||
self.smpl_group,
|
||||
],
|
||||
)
|
||||
|
||||
self._layout.addWidget(self.input_group)
|
||||
self._layout.addStretch()
|
||||
|
||||
def _create_smpl(self, beamline: BeamlineId) -> Union[InputNumberField, ComboBox]:
|
||||
match beamline:
|
||||
case BeamlineId.X01DA:
|
||||
return InputNumberField(
|
||||
"smpl",
|
||||
"Sample Position",
|
||||
unit="mm",
|
||||
init=23511,
|
||||
decimals=0,
|
||||
single_step=100,
|
||||
ll=23000,
|
||||
hl=30000,
|
||||
)
|
||||
case BeamlineId.X10DA:
|
||||
return ComboBox("smpl", "Sample Position", ["ES1", "ES2"])
|
||||
|
||||
@@ -343,6 +343,16 @@ class MoverPanel(QWidget):
|
||||
deadband=5,
|
||||
)
|
||||
self.mover_widgets.append(self.ot_es1_trz)
|
||||
self.es1man_trz = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="es1man_trz",
|
||||
label="MANIP TRZ",
|
||||
unit="mm",
|
||||
decimals=2,
|
||||
deadband=0.1,
|
||||
)
|
||||
self.mover_widgets.append(self.es1man_trz)
|
||||
|
||||
if beamline == "x10da":
|
||||
self.es1_try = MoveWidget(
|
||||
@@ -390,13 +400,25 @@ class MoverPanel(QWidget):
|
||||
self.mover_widgets.append(self.es1ic2_try)
|
||||
|
||||
if beamline == "x01da":
|
||||
self.es1_mov_group = Group("Experimental Station 1", [self.ot_es1_trz])
|
||||
self.es1_mov_group = Group("Experimental Station 1", [self.ot_es1_trz, self.es1man_trz])
|
||||
else:
|
||||
self.es1_mov_group = Group(
|
||||
"Experimental Station 1", [self.es1_try, self.es1ic1_try, self.es1ic2_try]
|
||||
)
|
||||
|
||||
# Experimental Station 2
|
||||
if beamline == "x01da":
|
||||
self.ot_es2_trz = MoveWidget(
|
||||
beamline=beamline,
|
||||
dev=dev,
|
||||
motor="ot_es2_trz",
|
||||
label="ES2 TRZ",
|
||||
unit="mm",
|
||||
decimals=0,
|
||||
deadband=5,
|
||||
)
|
||||
self.mover_widgets.append(self.ot_es2_trz)
|
||||
|
||||
if beamline == "x10da":
|
||||
self.es2_try = MoveWidget(
|
||||
beamline=beamline,
|
||||
@@ -409,6 +431,9 @@ class MoverPanel(QWidget):
|
||||
)
|
||||
self.mover_widgets.append(self.es2_try)
|
||||
|
||||
if beamline == "x01da":
|
||||
self.es2_mov_group = Group("Experimental Station 2", [self.ot_es2_trz])
|
||||
else:
|
||||
self.es2_mov_group = Group("Experimental Station 2", [self.es2_try])
|
||||
|
||||
# Assemble complete mover group
|
||||
@@ -428,6 +453,7 @@ class MoverPanel(QWidget):
|
||||
self.ot_mov_group,
|
||||
self.es0_mov_group,
|
||||
self.es1_mov_group,
|
||||
self.es2_mov_group,
|
||||
],
|
||||
)
|
||||
else:
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""Move widget to display an axis and also move it through BEC"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import Literal, Optional
|
||||
|
||||
from bec_lib import bec_logger
|
||||
@@ -11,15 +10,13 @@ from bec_widgets.utils.colors import get_accent_colors
|
||||
# pylint: disable=E0611
|
||||
from qtpy.QtCore import Property # type: ignore[attr-defined]
|
||||
from qtpy.QtCore import Signal # type: ignore[attr-defined]
|
||||
from qtpy.QtCore import QObject, QPropertyAnimation, Qt, QThread
|
||||
from qtpy.QtCore import QObject, QPropertyAnimation, Qt
|
||||
from qtpy.QtGui import QTransform
|
||||
from qtpy.QtWidgets import QApplication, QHBoxLayout, QLabel, QPushButton, QWidget
|
||||
|
||||
from .....bec_ipython_client.plugins.digital_twin_core.motion_worker import MotionWorker
|
||||
from .....bec_ipython_client.plugins.digital_twin_core.types import BeamlineId
|
||||
|
||||
# pylint: disable=E0402
|
||||
from .....devices.absorber import STATUS as ABS_STATUS
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
@@ -124,222 +121,13 @@ class StatusIcon(QWidget):
|
||||
self._label.setPixmap(icon)
|
||||
|
||||
|
||||
class MotionWorker(QObject):
|
||||
"""
|
||||
Executes motion on the specified motor and includes some safety during
|
||||
motion for certain motors.
|
||||
"""
|
||||
class _WorkerSignals(QObject):
|
||||
"""Tiny QObject just to own signals, since MotionWorker itself is no longer a QObject."""
|
||||
|
||||
position_changed = Signal(float)
|
||||
error = Signal()
|
||||
finished = Signal()
|
||||
|
||||
def __init__(self, beamline: BeamlineId, dev, motor, target_pos: float):
|
||||
super().__init__()
|
||||
self.beamline = beamline
|
||||
self.dev = dev
|
||||
self.motor = motor
|
||||
self._target = target_pos
|
||||
self._stop_flag = threading.Event()
|
||||
|
||||
def stop(self):
|
||||
"""Sets the stop flag"""
|
||||
self._stop_flag.set()
|
||||
|
||||
def run(self):
|
||||
"""Prepares the movement based on the axis (motor)"""
|
||||
match self.motor:
|
||||
case "sldi_gapx" | "sldi_gapy" | "sldi_centerx" | "sldi_centery":
|
||||
self.motion()
|
||||
case "cm_trx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[{"device": self.dev["cm_roty"], "abs_tol": 0.05}],
|
||||
)
|
||||
case "cm_roty":
|
||||
self.motion(
|
||||
abs_closed=True, surveyed_axes=[{"device": self.dev["cm_trx"], "abs_tol": 0.05}]
|
||||
)
|
||||
case "cm_try":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["cm_rotx"], "abs_tol": 0.05},
|
||||
{"device": self.dev["cm_rotz"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "cm_rotx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["cm_try"], "abs_tol": 0.05},
|
||||
{"device": self.dev["cm_rotz"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "cm_rotz":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["cm_try"], "abs_tol": 0.05},
|
||||
{"device": self.dev["cm_rotx"], "abs_tol": 0.05},
|
||||
],
|
||||
)
|
||||
case "cm_bnd":
|
||||
if self.beamline == "x01da":
|
||||
p1 = (
|
||||
1 / (self.dev.cm_bnd_radius.read()["cm_bnd_radius"]["value"] * 1e3) + 0.0284
|
||||
) / 2e-6
|
||||
p2 = (1 / (self._target * 1e3) + 0.0284) / 2e-6
|
||||
else:
|
||||
p1 = 541900 / self.dev.cm_bnd_radius.read()["cm_bnd_radius"]["value"] - 32570
|
||||
p2 = 541900 / self._target - 32570
|
||||
self._target = p2 - p1
|
||||
self.motion(relative=True, rb={"device": self.dev["cm_bnd_radius"]})
|
||||
case "mo1_try" | "mo1_trx" | "mo1_roty":
|
||||
self.motion(abs_closed=True)
|
||||
case "mo1_bragg_angle":
|
||||
if self.beamline == "x01da":
|
||||
self.motion()
|
||||
else: # x10da needs to move goniometer
|
||||
self.motion(alias="mo1_rotx")
|
||||
case "sl1_centery" | "sl1_gapy" | "bm1_try":
|
||||
self.motion()
|
||||
case "fm_trx":
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[{"device": self.dev["fm_roty"], "abs_tol": 0.05}],
|
||||
)
|
||||
case "fm_roty":
|
||||
self.motion(
|
||||
abs_closed=True, surveyed_axes=[{"device": self.dev["fm_trx"], "abs_tol": 0.05}]
|
||||
)
|
||||
case "fm_try":
|
||||
if self.beamline == "x01da":
|
||||
abs_tol = 0.05
|
||||
else: # superxas mirror less stable thus needs higher tolerance
|
||||
abs_tol = 0.2
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["fm_rotx"], "abs_tol": abs_tol},
|
||||
{"device": self.dev["fm_rotz"], "abs_tol": abs_tol},
|
||||
],
|
||||
)
|
||||
case "fm_rotx":
|
||||
if self.beamline == "x01da":
|
||||
abs_tol = 0.05
|
||||
else: # superxas mirror less stable thus needs higher tolerance
|
||||
abs_tol = 0.2
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["fm_try"], "abs_tol": abs_tol},
|
||||
{"device": self.dev["fm_rotz"], "abs_tol": abs_tol},
|
||||
],
|
||||
)
|
||||
case "fm_rotz":
|
||||
if self.beamline == "x01da":
|
||||
abs_tol = 0.05
|
||||
else: # superxas mirror less stable thus needs higher tolerance
|
||||
abs_tol = 0.2
|
||||
self.motion(
|
||||
abs_closed=True,
|
||||
surveyed_axes=[
|
||||
{"device": self.dev["fm_try"], "abs_tol": abs_tol},
|
||||
{"device": self.dev["fm_rotx"], "abs_tol": abs_tol},
|
||||
],
|
||||
)
|
||||
case "fm_bnd":
|
||||
if self.beamline == "x01da":
|
||||
p1 = (
|
||||
1 / (self.dev.fm_bnd_radius.read()["fm_bnd_radius"]["value"] * 1e3)
|
||||
+ 4.28e-5
|
||||
) / 1.84e-9
|
||||
p2 = (1 / (self._target * 1e3) + 4.28e-5) / 1.84e-9
|
||||
else:
|
||||
p1 = (
|
||||
593088.7 / self.dev.fm_bnd_radius.read()["fm_bnd_radius"]["value"]
|
||||
+ 26124.41
|
||||
)
|
||||
p2 = 593088.7 / self._target + 26124.41
|
||||
self._target = p2 - p1
|
||||
self.motion(relative=True, rb={"device": self.dev["fm_bnd_radius"]})
|
||||
case "sl2_centery" | "sl2_gapy" | "bm2_try":
|
||||
self.motion()
|
||||
case "ot_try" | "ot_rotx" | "ot_es1_trz":
|
||||
self.motion()
|
||||
case "es0wi_try":
|
||||
self.motion()
|
||||
case "es1_try" | "es2_try":
|
||||
self.motion()
|
||||
case "es1ic0_try" | "es1ic1_try" | "es1ic2_try":
|
||||
self.motion()
|
||||
case _:
|
||||
logger.warning(f"Motor {self.motor} not integrated in digital twin!")
|
||||
|
||||
def motion(
|
||||
self,
|
||||
abs_closed: bool = False,
|
||||
relative: bool = False,
|
||||
rb=None,
|
||||
surveyed_axes=None,
|
||||
alias=None,
|
||||
):
|
||||
"""
|
||||
Moves an axis while surverying a set of axes (if set).
|
||||
Example surveyed_axes:
|
||||
[{'device': bec_device_object, 'abs_tol': 0.1},]
|
||||
|
||||
Args:
|
||||
surveyed_axes (list): List of dictionaries of devices
|
||||
"""
|
||||
logger.info(f"Move axis {self.motor} to target {self._target}, move_relative={relative}")
|
||||
try:
|
||||
if alias:
|
||||
self.motor = alias
|
||||
if abs_closed:
|
||||
if self.dev.abs.status.get() == ABS_STATUS.OPEN:
|
||||
status = self.dev.abs.close()
|
||||
status.wait(timeout=5)
|
||||
if surveyed_axes is not None:
|
||||
for surv_ax in surveyed_axes:
|
||||
surv_ax["name"] = surv_ax["device"].dotted_name
|
||||
surv_ax["old_value"] = surv_ax["device"].read(cached=True)[surv_ax["name"]][
|
||||
"value"
|
||||
]
|
||||
if rb is not None:
|
||||
rb["name"] = rb["device"].dotted_name
|
||||
status = self.dev[self.motor].move(self._target, relative=relative)
|
||||
last_check = time.time()
|
||||
update_interval = 0.1
|
||||
while status.status == "RUNNING":
|
||||
now = time.time()
|
||||
if time.time() - last_check < update_interval:
|
||||
time.sleep(0.01)
|
||||
last_check = now
|
||||
if self._stop_flag.is_set():
|
||||
self.dev[self.motor].stop()
|
||||
self._stop_flag.clear()
|
||||
if rb is not None:
|
||||
self.position_changed.emit(rb["device"].read(cached=True)[rb["name"]]["value"])
|
||||
else:
|
||||
self.position_changed.emit(
|
||||
self.dev[self.motor].read(cached=True)[self.motor]["value"]
|
||||
)
|
||||
if surveyed_axes is not None:
|
||||
for surv_ax in surveyed_axes:
|
||||
fb = surv_ax["device"].read(cached=True)[surv_ax["name"]]["value"]
|
||||
if abs(fb - surv_ax["old_value"]) > surv_ax["abs_tol"]:
|
||||
self.dev[self.motor].stop()
|
||||
self.error.emit()
|
||||
self.finished.emit()
|
||||
break
|
||||
self.finished.emit()
|
||||
except Exception as e:
|
||||
logger.error(f"Error during movement of {self.motor}: {e}")
|
||||
self.error.emit()
|
||||
self.finished.emit()
|
||||
|
||||
|
||||
class MoveWidget(QWidget):
|
||||
"""
|
||||
@@ -361,8 +149,9 @@ class MoveWidget(QWidget):
|
||||
self.motor = motor
|
||||
self.deadband = deadband
|
||||
self.status = Status.IN_POSITION
|
||||
self._thread: QThread | None = None
|
||||
self._thread: threading.Thread | None = None
|
||||
self._worker: MotionWorker | None = None
|
||||
self._signals: _WorkerSignals | None = None
|
||||
|
||||
self.text_color = (0, 0, 0)
|
||||
|
||||
@@ -488,7 +277,7 @@ class MoveWidget(QWidget):
|
||||
|
||||
def _on_button_clicked(self):
|
||||
"""Starts or stops motion depending on current situation"""
|
||||
if self._thread and self._thread.isRunning():
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._stop_motion()
|
||||
else:
|
||||
self._start_motion()
|
||||
@@ -503,18 +292,22 @@ class MoveWidget(QWidget):
|
||||
self._set_status(Status.MOVING)
|
||||
self._apply_button_style("stop")
|
||||
|
||||
self._worker = MotionWorker(self.beamline, self.dev, self.motor, target)
|
||||
self._thread = QThread()
|
||||
self._worker.moveToThread(self._thread)
|
||||
|
||||
self._thread.started.connect(self._worker.run)
|
||||
self._worker.position_changed.connect(self._on_position_changed)
|
||||
self._worker.error.connect(self._on_error)
|
||||
self._worker.error.connect(self._thread.quit)
|
||||
self._worker.finished.connect(self._on_motion_finished)
|
||||
self._worker.finished.connect(self._thread.quit)
|
||||
self._thread.finished.connect(self._cleanup_thread)
|
||||
self._signals = _WorkerSignals()
|
||||
self._signals.position_changed.connect(self._on_position_changed)
|
||||
self._signals.error.connect(self._on_error)
|
||||
self._signals.finished.connect(self._on_motion_finished)
|
||||
self._signals.finished.connect(self._cleanup_thread)
|
||||
|
||||
self._worker = MotionWorker(
|
||||
self.beamline,
|
||||
self.dev,
|
||||
self.motor,
|
||||
target,
|
||||
on_position_changed=self._signals.position_changed.emit,
|
||||
on_error=self._signals.error.emit,
|
||||
on_finished=self._signals.finished.emit,
|
||||
)
|
||||
self._thread = threading.Thread(target=self._worker.run, daemon=True)
|
||||
self._thread.start()
|
||||
|
||||
def _on_error(self):
|
||||
@@ -547,20 +340,16 @@ class MoveWidget(QWidget):
|
||||
|
||||
def _cleanup_thread(self):
|
||||
"""Cleaning up of the mover thread"""
|
||||
if self._thread:
|
||||
self._thread.deleteLater()
|
||||
self._thread = None
|
||||
if self._worker:
|
||||
self._worker.deleteLater()
|
||||
self._worker = None
|
||||
self._thread = None
|
||||
self._worker = None
|
||||
self._signals = None
|
||||
|
||||
def shutdown(self):
|
||||
"""Cleaning up of the mover when shutting down the application"""
|
||||
if self._worker:
|
||||
self._worker.stop()
|
||||
if self._thread:
|
||||
self._thread.quit()
|
||||
self._thread.wait(2000) # max 2 s grace period
|
||||
if self._thread and self._thread.is_alive():
|
||||
self._thread.join(2.0) # max 2 s grace period
|
||||
|
||||
|
||||
class AbsorberWidget(QWidget):
|
||||
|
||||
Reference in New Issue
Block a user