Compare commits
23 Commits
fix/refact
...
fix/contro
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c5a5c3d98 | |||
| ae20de9391 | |||
| 2cf2f4b4e4 | |||
| 1a9a0beb86 | |||
| 9f9aef348a | |||
|
|
f7a313b37f | ||
|
7326c471f8
|
|||
|
4b95ebace3
|
|||
|
c1dee287b8
|
|||
|
dd3b0144b9
|
|||
| 149af32ab1 | |||
|
|
47f0b66791 | ||
| 2c0fced9b7 | |||
| d99b44b619 | |||
|
|
dbab981ac2 | ||
|
|
bdd7f1767f | ||
| 0f41648053 | |||
| ec45bb4c33 | |||
| ac8177a132 | |||
| 36e8d87411 | |||
| f56a834db5 | |||
| 90d2c99c4a | |||
| 6a4bfc73f6 |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -91,9 +91,9 @@ class flomniGuiTools:
|
||||
print("Cannot open camera_overview. Device does not exist.")
|
||||
|
||||
def flomnigui_remove_all_docks(self):
|
||||
dev.cam_flomni_overview.stop_live_mode()
|
||||
dev.cam_flomni_gripper.stop_live_mode()
|
||||
dev.cam_xeye.live_mode = False
|
||||
#dev.cam_flomni_overview.stop_live_mode()
|
||||
#dev.cam_flomni_gripper.stop_live_mode()
|
||||
#dev.cam_xeye.live_mode = False
|
||||
self.gui.flomni.delete_all()
|
||||
self.progressbar = None
|
||||
self.text_box = None
|
||||
@@ -114,6 +114,58 @@ class flomniGuiTools:
|
||||
)
|
||||
idle_text_box.set_html_text(text)
|
||||
|
||||
def flomnigui_docs(self, filename: str | None = None):
|
||||
import csaxs_bec
|
||||
from pathlib import Path
|
||||
|
||||
print("The general flOMNI documentation is at \nhttps://sls-csaxs.readthedocs.io/en/latest/user/ptychography/flomni.html#user-ptychography-flomni")
|
||||
|
||||
csaxs_bec_basepath = Path(csaxs_bec.__file__).parent
|
||||
docs_folder = (
|
||||
csaxs_bec_basepath /
|
||||
"bec_ipython_client" / "plugins" / "flomni" / "docs"
|
||||
)
|
||||
|
||||
if not docs_folder.is_dir():
|
||||
raise NotADirectoryError(f"Docs folder not found: {docs_folder}")
|
||||
|
||||
pdfs = sorted(docs_folder.glob("*.pdf"))
|
||||
if not pdfs:
|
||||
raise FileNotFoundError(f"No PDF files found in {docs_folder}")
|
||||
|
||||
# --- Resolve PDF ------------------------------------------------------
|
||||
if filename is not None:
|
||||
pdf_file = docs_folder / filename
|
||||
if not pdf_file.exists():
|
||||
raise FileNotFoundError(f"Requested file not found: {filename}")
|
||||
else:
|
||||
print("\nAvailable flOMNI documentation PDFs:\n")
|
||||
for i, pdf in enumerate(pdfs, start=1):
|
||||
print(f" {i:2d}) {pdf.name}")
|
||||
print()
|
||||
|
||||
while True:
|
||||
try:
|
||||
choice = int(input(f"Select a file (1–{len(pdfs)}): "))
|
||||
if 1 <= choice <= len(pdfs):
|
||||
pdf_file = pdfs[choice - 1]
|
||||
break
|
||||
print(f"Enter a number between 1 and {len(pdfs)}.")
|
||||
except ValueError:
|
||||
print("Invalid input. Please enter a number.")
|
||||
|
||||
# --- GUI handling (active existence check) ----------------------------
|
||||
self.flomnigui_show_gui()
|
||||
|
||||
if self._flomnigui_check_attribute_not_exists("PdfViewerWidget"):
|
||||
self.flomnigui_remove_all_docks()
|
||||
self.pdf_viewer = self.gui.flomni.new(widget="PdfViewerWidget")
|
||||
|
||||
# --- Load PDF ---------------------------------------------------------
|
||||
self.pdf_viewer.PdfViewerWidget.load_pdf(str(pdf_file.resolve()))
|
||||
print(f"\nLoaded: {pdf_file.name}\n")
|
||||
|
||||
|
||||
def _flomnicam_check_device_exists(self, device):
|
||||
try:
|
||||
device
|
||||
@@ -157,7 +209,7 @@ class flomniGuiTools:
|
||||
self.progressbar.set_value([progress, subtomo_progress, 0])
|
||||
if self.text_box is not None:
|
||||
text = f"Progress report:\n Tomo type: ....................... {self.progress['tomo_type']}\n Projection: ...................... {self.progress['projection']:.0f}\n Total projections expected ....... {self.progress['total_projections']}\n Angle: ........................... {self.progress['angle']}\n Current subtomo: ................. {self.progress['subtomo']}\n Current projection within subtomo: {self.progress['subtomo_projection']}\n Total projections per subtomo: ... {self.progress['subtomo_total_projections']}"
|
||||
self.text_box.set_plain_text(text)
|
||||
self.text_box.set_plain_text(text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -17,7 +17,7 @@ umv = builtins.__dict__.get("umv")
|
||||
umvr = builtins.__dict__.get("umvr")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_ipython_client.plugins.flomni.flomni import Flomni
|
||||
from bec_ipython_client.plugins.flomni import Flomni
|
||||
|
||||
|
||||
class XrayEyeAlign:
|
||||
|
||||
@@ -61,25 +61,3 @@ bec._beamline_mixin._bl_info_register(OperatorInfo)
|
||||
# SETUP PROMPTS
|
||||
bec._ip.prompts.session_name = _session_name
|
||||
bec._ip.prompts.status = 1
|
||||
|
||||
# REGISTER BEAMLINE CHECKS
|
||||
from bec_lib.bl_conditions import (
|
||||
FastOrbitFeedbackCondition,
|
||||
LightAvailableCondition,
|
||||
ShutterCondition,
|
||||
)
|
||||
|
||||
if "sls_machine_status" in dev:
|
||||
print("Registering light available condition for SLS machine status")
|
||||
_light_available_condition = LightAvailableCondition(dev.sls_machine_status)
|
||||
bec.bl_checks.register(_light_available_condition)
|
||||
|
||||
if "x12sa_es1_shutter_status" in dev:
|
||||
print("Registering shutter condition for X12SA ES1 shutter status")
|
||||
_shutter_condition = ShutterCondition(dev.x12sa_es1_shutter_status)
|
||||
bec.bl_checks.register(_shutter_condition)
|
||||
|
||||
# if hasattr(dev, "sls_fast_orbit_feedback"):
|
||||
# print("Registering fast orbit feedback condition for SLS fast orbit feedback")
|
||||
# _fast_orbit_feedback_condition = FastOrbitFeedbackCondition(dev.sls_fast_orbit_feedback)
|
||||
# bec.bl_checks.register(_fast_orbit_feedback_condition)
|
||||
|
||||
@@ -115,7 +115,7 @@ samy:
|
||||
softwareTrigger: false
|
||||
micfoc:
|
||||
description: Focusing motor of Microscope stage
|
||||
deviceClass: ophyd_devices.devices.EpicsMotorEx
|
||||
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES06
|
||||
motor_resolution: 0.00125
|
||||
@@ -133,7 +133,7 @@ micfoc:
|
||||
softwareTrigger: false
|
||||
owis_samx:
|
||||
description: Owis motor stage samx
|
||||
deviceClass: ophyd_devices.devices.EpicsMotorEx
|
||||
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES01
|
||||
motor_resolution: 0.00125
|
||||
@@ -151,7 +151,7 @@ owis_samx:
|
||||
softwareTrigger: false
|
||||
owis_samy:
|
||||
description: Owis motor stage samx
|
||||
deviceClass: ophyd_devices.devices.EpicsMotorEx
|
||||
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES02
|
||||
motor_resolution: 0.00125
|
||||
@@ -169,7 +169,7 @@ owis_samy:
|
||||
softwareTrigger: false
|
||||
rotx:
|
||||
description: Rotation stage rotx
|
||||
deviceClass: ophyd_devices.devices.EpicsMotorEx
|
||||
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES05
|
||||
motor_resolution: 0.0025
|
||||
@@ -190,7 +190,7 @@ rotx:
|
||||
softwareTrigger: false
|
||||
roty:
|
||||
description: Rotation stage rotx
|
||||
deviceClass: ophyd_devices.devices.EpicsMotorEx
|
||||
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES04
|
||||
motor_resolution: 0.0025
|
||||
|
||||
@@ -365,18 +365,6 @@ rtz:
|
||||
readOnly: false
|
||||
readoutPriority: on_request
|
||||
|
||||
rt_flyer:
|
||||
description: flomni rt flyer
|
||||
deviceClass: csaxs_bec.devices.omny.rt.rt_flomni_ophyd.RtFlomniFlyer
|
||||
deviceConfig:
|
||||
host: mpc2844.psi.ch
|
||||
port: 2222
|
||||
enabled: true
|
||||
onFailure: buffer
|
||||
readOnly: false
|
||||
readoutPriority: async
|
||||
|
||||
|
||||
############################################################
|
||||
####################### Cameras ############################
|
||||
############################################################
|
||||
|
||||
96
csaxs_bec/device_configs/sastt.yaml
Normal file
96
csaxs_bec/device_configs/sastt.yaml
Normal file
@@ -0,0 +1,96 @@
|
||||
samx:
|
||||
description: Owis motor stage samx
|
||||
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES01
|
||||
motor_resolution: 0.00125
|
||||
base_velocity: 0.0625
|
||||
velocity: 10
|
||||
backlash_distance: 0.125
|
||||
acceleration: 0.2
|
||||
user_offset_dir: 1
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- owis_samx
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
samy:
|
||||
description: Owis motor stage samy
|
||||
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES02
|
||||
motor_resolution: 0.00125
|
||||
base_velocity: 0.0625
|
||||
velocity: 10
|
||||
backlash_distance: 0.125
|
||||
acceleration: 0.2
|
||||
user_offset_dir: 0
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- owis_samx
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
rotx:
|
||||
description: Rotation stage rotx
|
||||
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES03
|
||||
motor_resolution: 0.0025
|
||||
base_velocity: 0.5
|
||||
velocity: 7.5
|
||||
backlash_distance: 0.25
|
||||
acceleration: 0.2
|
||||
user_offset_dir: 1
|
||||
limits:
|
||||
- -0.1
|
||||
- 0.1
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- rotx
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
roty:
|
||||
description: Rotation stage roty
|
||||
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES04
|
||||
motor_resolution: 0.0025
|
||||
base_velocity: 0.5
|
||||
velocity: 7.5
|
||||
backlash_distance: 0.25
|
||||
acceleration: 0.2
|
||||
user_offset_dir: 0
|
||||
limits:
|
||||
- -0.1
|
||||
- 0.1
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- roty
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
micfoc:
|
||||
description: Focusing motor of Microscope stage
|
||||
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
|
||||
deviceConfig:
|
||||
prefix: X12SA-ES2-ES05
|
||||
motor_resolution: 0.00125
|
||||
base_velocity: 0.25
|
||||
velocity: 2.5
|
||||
backlash_distance: 0.125
|
||||
acceleration: 0.4
|
||||
user_offset_dir: 0
|
||||
deviceTags:
|
||||
- cSAXS
|
||||
- micfoc
|
||||
onFailure: buffer
|
||||
enabled: true
|
||||
readoutPriority: baseline
|
||||
softwareTrigger: false
|
||||
@@ -37,8 +37,7 @@ import traceback
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import DeviceStatus
|
||||
from ophyd_devices import CompareStatus, TransitionStatus
|
||||
from ophyd_devices import CompareStatus, DeviceStatus, TransitionStatus
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import (
|
||||
|
||||
@@ -143,7 +143,7 @@ class StatusBitsCompareStatus(SubscriptionStatus):
|
||||
self._add_delay = add_delay or 0
|
||||
self._raise_states = raise_states or []
|
||||
super().__init__(
|
||||
device=signal,
|
||||
obj=signal,
|
||||
callback=self._compare_callback,
|
||||
timeout=timeout,
|
||||
settle_time=settle_time,
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
"""Falcon Sitoro detector class for cSAXS beamline."""
|
||||
|
||||
import enum
|
||||
import os
|
||||
import threading
|
||||
from typing import Literal
|
||||
|
||||
from bec_lib.file_utils import get_full_path
|
||||
from bec_lib.logger import bec_logger
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
|
||||
from ophyd.mca import EpicsMCARecord
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin,
|
||||
PSIDetectorBase,
|
||||
)
|
||||
from ophyd_devices import CompareStatus, FileEventSignal
|
||||
from ophyd_devices.devices.areadetector.plugins import HDF5Plugin_V35 as HDF5Plugin
|
||||
from ophyd_devices.devices.dxp import EpicsDXPFalcon, EpicsMCARecord, Falcon
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
@@ -18,15 +20,11 @@ class FalconError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
|
||||
class FalconTimeoutError(FalconError):
|
||||
"""Raised when the Falcon does not respond in time."""
|
||||
|
||||
|
||||
class DetectorState(enum.IntEnum):
|
||||
class ACQUIRESTATUS(enum.IntEnum):
|
||||
"""Detector states for Falcon detector"""
|
||||
|
||||
DONE = 0
|
||||
ACQUIRING = 1
|
||||
ACQUIRING = 1 # or Capturing
|
||||
|
||||
|
||||
class TriggerSource(enum.IntEnum):
|
||||
@@ -44,238 +42,56 @@ class MappingSource(enum.IntEnum):
|
||||
MAPPING = 1
|
||||
|
||||
|
||||
class EpicsDXPFalcon(Device):
|
||||
"""
|
||||
DXP parameters for Falcon detector
|
||||
class FalconControl(Falcon):
|
||||
"""Falcon Control class at cSAXS. prefix: 'X12SA-SITORO:'"""
|
||||
|
||||
Base class to map EPICS PVs from DXP parameters to ophyd signals.
|
||||
dxp = Cpt(EpicsDXPFalcon, "dxp1:")
|
||||
mca = Cpt(EpicsMCARecord, "mca1")
|
||||
hdf5 = Cpt(HDF5Plugin, "HDF1:")
|
||||
|
||||
|
||||
class FalconcSAXS(PSIDeviceBase, FalconControl):
|
||||
"""
|
||||
Falcon Sitoro detector for CSAXS
|
||||
|
||||
|
||||
class attributes:
|
||||
dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector
|
||||
mca (EpicsMCARecord) : MCA parameters for Falcon detector
|
||||
hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector
|
||||
MIN_READOUT (float) : Minimum readout time for the detector
|
||||
"""
|
||||
|
||||
elapsed_live_time = Cpt(EpicsSignal, "ElapsedLiveTime")
|
||||
elapsed_real_time = Cpt(EpicsSignal, "ElapsedRealTime")
|
||||
elapsed_trigger_live_time = Cpt(EpicsSignal, "ElapsedTriggerLiveTime")
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 3e-3
|
||||
_pv_timeout = 3 # Timeout for PV operations in seconds
|
||||
|
||||
# Energy Filter PVs
|
||||
energy_threshold = Cpt(EpicsSignalWithRBV, "DetectionThreshold")
|
||||
min_pulse_separation = Cpt(EpicsSignalWithRBV, "MinPulsePairSeparation")
|
||||
detection_filter = Cpt(EpicsSignalWithRBV, "DetectionFilter", string=True)
|
||||
scale_factor = Cpt(EpicsSignalWithRBV, "ScaleFactor")
|
||||
risetime_optimisation = Cpt(EpicsSignalWithRBV, "RisetimeOptimization")
|
||||
|
||||
# Misc PVs
|
||||
detector_polarity = Cpt(EpicsSignalWithRBV, "DetectorPolarity")
|
||||
decay_time = Cpt(EpicsSignalWithRBV, "DecayTime")
|
||||
|
||||
current_pixel = Cpt(EpicsSignalRO, "CurrentPixel")
|
||||
|
||||
|
||||
class FalconHDF5Plugins(Device):
|
||||
"""
|
||||
HDF5 parameters for Falcon detector
|
||||
|
||||
Base class to map EPICS PVs from HDF5 Plugin to ophyd signals.
|
||||
"""
|
||||
|
||||
capture = Cpt(EpicsSignalWithRBV, "Capture")
|
||||
enable = Cpt(EpicsSignalWithRBV, "EnableCallbacks", string=True, kind="config")
|
||||
xml_file_name = Cpt(EpicsSignalWithRBV, "XMLFileName", string=True, kind="config")
|
||||
lazy_open = Cpt(EpicsSignalWithRBV, "LazyOpen", string=True, doc="0='No' 1='Yes'")
|
||||
temp_suffix = Cpt(EpicsSignalWithRBV, "TempSuffix", string=True)
|
||||
file_path = Cpt(EpicsSignalWithRBV, "FilePath", string=True, kind="config")
|
||||
file_name = Cpt(EpicsSignalWithRBV, "FileName", string=True, kind="config")
|
||||
file_template = Cpt(EpicsSignalWithRBV, "FileTemplate", string=True, kind="config")
|
||||
num_capture = Cpt(EpicsSignalWithRBV, "NumCapture", kind="config")
|
||||
file_write_mode = Cpt(EpicsSignalWithRBV, "FileWriteMode", kind="config")
|
||||
queue_size = Cpt(EpicsSignalWithRBV, "QueueSize", kind="config")
|
||||
array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config")
|
||||
|
||||
|
||||
class FalconSetup(CustomDetectorMixin):
|
||||
"""
|
||||
Falcon setup class for cSAXS
|
||||
|
||||
Parent class: CustomDetectorMixin
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
|
||||
super().__init__(*args, parent=parent, **kwargs)
|
||||
self._lock = threading.RLock()
|
||||
file_event = Cpt(FileEventSignal, name="file_event")
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""Initialize Falcon detector"""
|
||||
self.initialize_default_parameter()
|
||||
self.initialize_detector()
|
||||
self.initialize_detector_backend()
|
||||
"""Initialize Falcon Sitoro detector"""
|
||||
self._lock = threading.RLock()
|
||||
self._readout_time = self.MIN_READOUT
|
||||
self._value_pixel_per_buffer = 20
|
||||
self._queue_size = 2000
|
||||
self._full_path = ""
|
||||
|
||||
def initialize_default_parameter(self) -> None:
|
||||
def on_connected(self):
|
||||
"""
|
||||
Set default parameters for Falcon
|
||||
|
||||
This will set:
|
||||
- readout (float): readout time in seconds
|
||||
- value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro
|
||||
|
||||
Setup Falcon Sitoro detector default parameters once signals are connected
|
||||
"""
|
||||
self.parent.value_pixel_per_buffer = 20
|
||||
self.update_readout_time()
|
||||
|
||||
def update_readout_time(self) -> None:
|
||||
"""Set readout time for Eiger9M detector"""
|
||||
readout_time = (
|
||||
self.parent.scaninfo.readout_time
|
||||
if hasattr(self.parent.scaninfo, "readout_time")
|
||||
else self.parent.MIN_READOUT
|
||||
)
|
||||
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
|
||||
|
||||
def initialize_detector(self) -> None:
|
||||
"""Initialize Falcon detector"""
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
self.on_stop()
|
||||
self._initialize_detector()
|
||||
self._initialize_detector_backend()
|
||||
self.set_trigger(
|
||||
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
|
||||
)
|
||||
# 1 Realtime
|
||||
self.parent.preset_mode.put(1)
|
||||
# 0 Normal, 1 Inverted
|
||||
self.parent.input_logic_polarity.put(0)
|
||||
# 0 Manual 1 Auto
|
||||
self.parent.auto_pixels_per_buffer.put(0)
|
||||
# Sets the number of pixels/spectra in the buffer
|
||||
self.parent.pixels_per_buffer.put(self.parent.value_pixel_per_buffer)
|
||||
|
||||
def initialize_detector_backend(self) -> None:
|
||||
"""Initialize the detector backend for Falcon."""
|
||||
self.parent.hdf5.enable.put(1)
|
||||
# file location of h5 layout for cSAXS
|
||||
self.parent.hdf5.xml_file_name.put("layout.xml")
|
||||
# TODO Check if lazy open is needed and wanted!
|
||||
self.parent.hdf5.lazy_open.put(1)
|
||||
self.parent.hdf5.temp_suffix.put("")
|
||||
# size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost
|
||||
self.parent.hdf5.queue_size.put(2000)
|
||||
# Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate
|
||||
self.parent.nd_array_mode.put(1)
|
||||
|
||||
def on_stage(self) -> None:
|
||||
"""Prepare detector and backend for acquisition"""
|
||||
self.prepare_detector()
|
||||
self.prepare_data_backend()
|
||||
self.publish_file_location(done=False, successful=False)
|
||||
self.arm_acquisition()
|
||||
|
||||
def prepare_detector(self) -> None:
|
||||
"""Prepare detector for acquisition"""
|
||||
self.set_trigger(
|
||||
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
|
||||
)
|
||||
self.parent.preset_real.put(self.parent.scaninfo.exp_time)
|
||||
self.parent.pixels_per_run.put(
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
|
||||
)
|
||||
|
||||
def prepare_data_backend(self) -> None:
|
||||
"""Prepare data backend for acquisition"""
|
||||
self.parent.filepath.set(
|
||||
self.parent.filewriter.compile_full_filename(f"{self.parent.name}.h5")
|
||||
).wait()
|
||||
file_path, file_name = os.path.split(self.parent.filepath.get())
|
||||
self.parent.hdf5.file_path.put(file_path)
|
||||
self.parent.hdf5.file_name.put(file_name)
|
||||
self.parent.hdf5.file_template.put("%s%s")
|
||||
self.parent.hdf5.num_capture.put(
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
|
||||
)
|
||||
self.parent.hdf5.file_write_mode.put(2)
|
||||
# Reset spectrum counter in filewriter, used for indexing & identifying missing triggers
|
||||
self.parent.hdf5.array_counter.put(0)
|
||||
# Start file writing
|
||||
self.parent.hdf5.capture.put(1)
|
||||
|
||||
def arm_acquisition(self) -> None:
|
||||
"""Arm detector for acquisition"""
|
||||
self.parent.start_all.put(1)
|
||||
signal_conditions = [
|
||||
(
|
||||
lambda: self.parent.state.read()[self.parent.state.name]["value"],
|
||||
DetectorState.ACQUIRING,
|
||||
)
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.TIMEOUT_FOR_SIGNALS,
|
||||
check_stopped=True,
|
||||
all_signals=False,
|
||||
):
|
||||
raise FalconTimeoutError(
|
||||
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
|
||||
)
|
||||
|
||||
def on_unstage(self) -> None:
|
||||
"""Unstage detector and backend"""
|
||||
pass
|
||||
|
||||
def on_complete(self) -> None:
|
||||
"""Complete detector and backend"""
|
||||
self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS)
|
||||
self.publish_file_location(done=True, successful=True)
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Stop detector and backend"""
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
|
||||
def stop_detector(self) -> None:
|
||||
"""Stops detector"""
|
||||
|
||||
self.parent.stop_all.put(1)
|
||||
self.parent.erase_all.put(1)
|
||||
|
||||
signal_conditions = [
|
||||
(lambda: self.parent.state.read()[self.parent.state.name]["value"], DetectorState.DONE)
|
||||
]
|
||||
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=self.parent.TIMEOUT_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 2,
|
||||
all_signals=False,
|
||||
):
|
||||
# Retry stop detector and wait for remaining time
|
||||
raise FalconTimeoutError(
|
||||
f"Failed to stop detector, timeout with state {signal_conditions[0][0]}"
|
||||
)
|
||||
|
||||
def stop_detector_backend(self) -> None:
|
||||
"""Stop the detector backend"""
|
||||
self.parent.hdf5.capture.put(0)
|
||||
|
||||
def finished(self, timeout: int = 5) -> None:
|
||||
"""Check if scan finished succesfully"""
|
||||
with self._lock:
|
||||
total_frames = int(
|
||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
||||
)
|
||||
signal_conditions = [
|
||||
(self.parent.dxp.current_pixel.get, total_frames),
|
||||
(self.parent.hdf5.array_counter.get, total_frames),
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=timeout,
|
||||
check_stopped=True,
|
||||
all_signals=True,
|
||||
):
|
||||
logger.debug(
|
||||
f"Falcon missed a trigger: received trigger {self.parent.dxp.current_pixel.get()},"
|
||||
f" send data {self.parent.hdf5.array_counter.get()} from total_frames"
|
||||
f" {total_frames}"
|
||||
)
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
|
||||
def set_trigger(
|
||||
self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0
|
||||
self,
|
||||
mapping_mode: MappingSource,
|
||||
trigger_source: TriggerSource,
|
||||
ignore_gate: Literal[0, 1] = 0,
|
||||
) -> None:
|
||||
"""
|
||||
Set triggering mode for detector
|
||||
@@ -287,63 +103,140 @@ class FalconSetup(CustomDetectorMixin):
|
||||
|
||||
"""
|
||||
mapping = int(mapping_mode)
|
||||
trigger = trigger_source
|
||||
self.parent.collect_mode.put(mapping)
|
||||
self.parent.pixel_advance_mode.put(trigger)
|
||||
self.parent.ignore_gate.put(ignore_gate)
|
||||
trigger = int(trigger_source)
|
||||
self.collect_mode.put(mapping)
|
||||
self.pixel_advance_mode.put(trigger)
|
||||
self.ignore_gate.put(ignore_gate)
|
||||
|
||||
def _initialize_detector(self) -> None:
|
||||
"""Initialize Falcon detector"""
|
||||
|
||||
class FalconcSAXS(PSIDetectorBase):
|
||||
"""
|
||||
Falcon Sitoro detector for CSAXS
|
||||
# 1 Realtime
|
||||
self.preset_mode.put(1)
|
||||
|
||||
Parent class: PSIDetectorBase
|
||||
# 0 Normal, 1 Inverted
|
||||
self.input_logic_polarity.put(0)
|
||||
|
||||
class attributes:
|
||||
custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,
|
||||
inherits from CustomDetectorMixin
|
||||
PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector
|
||||
dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector
|
||||
mca (EpicsMCARecord) : MCA parameters for Falcon detector
|
||||
hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector
|
||||
MIN_READOUT (float) : Minimum readout time for the detector
|
||||
"""
|
||||
# 0 Manual 1 Auto
|
||||
self.auto_pixels_per_buffer.put(0)
|
||||
|
||||
# Specify which functions are revealed to the user in BEC client
|
||||
USER_ACCESS = ["describe"]
|
||||
# Sets the number of pixels/spectra in the buffer
|
||||
self.pixels_per_buffer.put(self._value_pixel_per_buffer)
|
||||
|
||||
# specify Setup class
|
||||
custom_prepare_cls = FalconSetup
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 3e-3
|
||||
TIMEOUT_FOR_SIGNALS = 5
|
||||
def _initialize_detector_backend(self) -> None:
|
||||
"""Initialize the detector backend for Falcon."""
|
||||
# Enable HDF5 plugin
|
||||
self.hdf5.enable.put(1)
|
||||
|
||||
# specify class attributes
|
||||
dxp = Cpt(EpicsDXPFalcon, "dxp1:")
|
||||
mca = Cpt(EpicsMCARecord, "mca1")
|
||||
hdf5 = Cpt(FalconHDF5Plugins, "HDF1:")
|
||||
# Use layout.xml file for cSAXS Falcon. FIXME:Should be checked if IOC runs on different host.
|
||||
self.hdf5.xml_file_name.put("layout.xml")
|
||||
|
||||
stop_all = Cpt(EpicsSignal, "StopAll")
|
||||
erase_all = Cpt(EpicsSignal, "EraseAll")
|
||||
start_all = Cpt(EpicsSignal, "StartAll")
|
||||
state = Cpt(EpicsSignal, "Acquiring")
|
||||
preset_mode = Cpt(EpicsSignal, "PresetMode") # 0 No preset 1 Real time 2 Events 3 Triggers
|
||||
preset_real = Cpt(EpicsSignal, "PresetReal")
|
||||
preset_events = Cpt(EpicsSignal, "PresetEvents")
|
||||
preset_triggers = Cpt(EpicsSignal, "PresetTriggers")
|
||||
triggers = Cpt(EpicsSignalRO, "MaxTriggers", lazy=True)
|
||||
events = Cpt(EpicsSignalRO, "MaxEvents", lazy=True)
|
||||
input_count_rate = Cpt(EpicsSignalRO, "MaxInputCountRate", lazy=True)
|
||||
output_count_rate = Cpt(EpicsSignalRO, "MaxOutputCountRate", lazy=True)
|
||||
collect_mode = Cpt(EpicsSignal, "CollectMode") # 0 MCA spectra, 1 MCA mapping
|
||||
pixel_advance_mode = Cpt(EpicsSignal, "PixelAdvanceMode")
|
||||
ignore_gate = Cpt(EpicsSignal, "IgnoreGate")
|
||||
input_logic_polarity = Cpt(EpicsSignal, "InputLogicPolarity")
|
||||
auto_pixels_per_buffer = Cpt(EpicsSignal, "AutoPixelsPerBuffer")
|
||||
pixels_per_buffer = Cpt(EpicsSignal, "PixelsPerBuffer")
|
||||
pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun")
|
||||
nd_array_mode = Cpt(EpicsSignal, "NDArrayMode")
|
||||
# TODO Check if lazy open is needed and wanted!
|
||||
self.hdf5.lazy_open.put(1)
|
||||
self.hdf5.temp_suffix.put("")
|
||||
|
||||
# Size of the queue for the number of spectra allowed in the buffer. If too small, data is lost at high throughput
|
||||
self.hdf5.queue_size.put(self._queue_size)
|
||||
self.hdf5.file_template.put("%s%s")
|
||||
self.hdf5.file_write_mode.put(2)
|
||||
|
||||
# Set nd_array mode to 1: This means segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate
|
||||
self.nd_array_mode.put(1)
|
||||
|
||||
def on_stage(self):
|
||||
"""
|
||||
This method is called when the detector is staged for acquisition.
|
||||
We use the information in scan_info.msg about the upcoming scan to set all relevant parameters on the detector.
|
||||
"""
|
||||
# Calculate relevant parameters
|
||||
num_points = self.scan_info.msg.num_points
|
||||
frames_per_trigger = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
|
||||
overall_frames = int(num_points * frames_per_trigger)
|
||||
exp_time = self.scan_info.msg.scan_parameters["exp_time"]
|
||||
self._full_path = get_full_path(self.scan_info.msg, self.name)
|
||||
|
||||
# Check that exposure time is larger than readout time
|
||||
readout_time = max(
|
||||
self.scan_info.msg.scan_parameters.get("readout_time", self.MIN_READOUT),
|
||||
self.MIN_READOUT,
|
||||
)
|
||||
if exp_time < readout_time:
|
||||
raise ValueError(
|
||||
f"Exposure time {exp_time} is less than minimum readout time {readout_time}"
|
||||
)
|
||||
|
||||
# TODO: Add h5_entries for linking the Falcon NEXUS entries with the master file
|
||||
self.file_event.put(file_path=self._full_path, done=False, successful=False)
|
||||
|
||||
self.preset_real_time.put(exp_time)
|
||||
self.pixels_per_run.put(overall_frames)
|
||||
|
||||
# Prepare detector backend PVs
|
||||
file_path, file_name = os.path.split(self._full_path)
|
||||
self.hdf5.file_path.put(file_path)
|
||||
self.hdf5.file_name.put(file_name)
|
||||
|
||||
self.hdf5.num_capture.put(overall_frames)
|
||||
# Reset spectrum counter in filewriter, used for indexing & identifying missing triggers
|
||||
self.hdf5.array_counter.put(0)
|
||||
|
||||
# Start file writing
|
||||
self.hdf5.capture.put(1)
|
||||
# Start the acquisition
|
||||
self.start_all.put(1)
|
||||
|
||||
def on_pre_scan(self):
|
||||
"""
|
||||
Method for actions just before the scan starts.
|
||||
"""
|
||||
status_camera = CompareStatus(
|
||||
self.acquire_busy, ACQUIRESTATUS.ACQUIRING, timeout=self._pv_timeout
|
||||
)
|
||||
status_writer = CompareStatus(
|
||||
self.hdf5.capture, ACQUIRESTATUS.ACQUIRING, timeout=self._pv_timeout
|
||||
)
|
||||
# Logical combine of statuses
|
||||
status = status_camera & status_writer
|
||||
self.cancel_on_stop(status)
|
||||
return status
|
||||
|
||||
def _complete_callback(self, status: CompareStatus) -> None:
|
||||
"""Callback for when the device completes a scan."""
|
||||
# FIXME Add proper h5 entries once checked
|
||||
if status.success:
|
||||
self.file_event.put(
|
||||
file_path=self._full_path, # pylint: disable:protected-access
|
||||
done=True,
|
||||
successful=True,
|
||||
)
|
||||
else:
|
||||
self.file_event.put(
|
||||
file_path=self._full_path, # pylint: disable:protected-access
|
||||
done=True,
|
||||
successful=False,
|
||||
)
|
||||
|
||||
def on_complete(self) -> None:
|
||||
"""Complete detector and backend"""
|
||||
# Calculate relevant parameters
|
||||
num_points = self.scan_info.msg.num_points
|
||||
frames_per_trigger = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
|
||||
overall_frames = int(num_points * frames_per_trigger)
|
||||
|
||||
status_detector = CompareStatus(self.dxp.current_pixel, overall_frames, run=True)
|
||||
status_backend = CompareStatus(self.hdf5.array_counter, overall_frames, run=True)
|
||||
|
||||
status = status_detector & status_backend
|
||||
self.cancel_on_stop(status)
|
||||
status.add_callback(self._complete_callback)
|
||||
return status
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Stop detector and backend"""
|
||||
self.stop_all.put(1)
|
||||
self.hdf5.capture.put(0)
|
||||
self.erase_all.put(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True)
|
||||
falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:")
|
||||
|
||||
@@ -1,400 +0,0 @@
|
||||
import enum
|
||||
import json
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
import requests
|
||||
from bec_lib import bec_logger
|
||||
from ophyd import ADComponent as ADCpt
|
||||
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Staged
|
||||
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
|
||||
CustomDetectorMixin,
|
||||
PSIDetectorBase,
|
||||
)
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class PilatusError(Exception):
|
||||
"""Base class for exceptions in this module."""
|
||||
|
||||
|
||||
class PilatusTimeoutError(PilatusError):
|
||||
"""Raised when the Pilatus does not respond in time during unstage."""
|
||||
|
||||
|
||||
class TriggerSource(enum.IntEnum):
|
||||
"""Trigger source options for the detector"""
|
||||
|
||||
INTERNAL = 0
|
||||
EXT_ENABLE = 1
|
||||
EXT_TRIGGER = 2
|
||||
MULTI_TRIGGER = 3
|
||||
ALGINMENT = 4
|
||||
|
||||
|
||||
class SLSDetectorCam(Device):
|
||||
"""SLS Detector Camera - Pilatus
|
||||
|
||||
Base class to map EPICS PVs to ophyd signals.
|
||||
"""
|
||||
|
||||
num_images = ADCpt(EpicsSignalWithRBV, "NumImages")
|
||||
num_frames = ADCpt(EpicsSignalWithRBV, "NumExposures")
|
||||
delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures")
|
||||
trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode")
|
||||
acquire = ADCpt(EpicsSignal, "Acquire")
|
||||
armed = ADCpt(EpicsSignalRO, "Armed")
|
||||
|
||||
read_file_timeout = ADCpt(EpicsSignal, "ImageFileTmot")
|
||||
detector_state = ADCpt(EpicsSignalRO, "StatusMessage_RBV")
|
||||
status_message_camserver = ADCpt(EpicsSignalRO, "StringFromServer_RBV", string=True)
|
||||
acquire_time = ADCpt(EpicsSignal, "AcquireTime")
|
||||
acquire_period = ADCpt(EpicsSignal, "AcquirePeriod")
|
||||
threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy")
|
||||
file_path = ADCpt(EpicsSignalWithRBV, "FilePath")
|
||||
file_name = ADCpt(EpicsSignalWithRBV, "FileName")
|
||||
file_number = ADCpt(EpicsSignalWithRBV, "FileNumber")
|
||||
auto_increment = ADCpt(EpicsSignalWithRBV, "AutoIncrement")
|
||||
file_template = ADCpt(EpicsSignalWithRBV, "FileTemplate")
|
||||
file_format = ADCpt(EpicsSignalWithRBV, "FileNumber")
|
||||
gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill")
|
||||
|
||||
|
||||
class PilatusSetup(CustomDetectorMixin):
|
||||
"""Pilatus setup class for cSAXS
|
||||
|
||||
Parent class: CustomDetectorMixin
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
|
||||
super().__init__(*args, parent=parent, **kwargs)
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def on_init(self) -> None:
|
||||
"""Initialize the detector"""
|
||||
self.initialize_default_parameter()
|
||||
self.initialize_detector()
|
||||
|
||||
def initialize_default_parameter(self) -> None:
|
||||
"""Set default parameters for Eiger9M detector"""
|
||||
self.update_readout_time()
|
||||
|
||||
def update_readout_time(self) -> None:
|
||||
"""Set readout time for Eiger9M detector"""
|
||||
readout_time = (
|
||||
self.parent.scaninfo.readout_time
|
||||
if hasattr(self.parent.scaninfo, "readout_time")
|
||||
else self.parent.MIN_READOUT
|
||||
)
|
||||
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
|
||||
|
||||
def initialize_detector(self) -> None:
|
||||
"""Initialize detector"""
|
||||
# Stops the detector
|
||||
self.stop_detector()
|
||||
# Sets the trigger source to GATING
|
||||
self.parent.cam.trigger_mode.put(TriggerSource.EXT_ENABLE)
|
||||
|
||||
def on_stage(self) -> None:
|
||||
"""Stage the detector for scan"""
|
||||
self.prepare_detector()
|
||||
self.prepare_data_backend()
|
||||
self.publish_file_location(
|
||||
done=False, successful=False, metadata={"input_path": self.parent.filepath_raw}
|
||||
)
|
||||
|
||||
def prepare_detector(self) -> None:
|
||||
"""
|
||||
Prepare detector for scan.
|
||||
|
||||
Includes checking the detector threshold,
|
||||
setting the acquisition parameters and setting the trigger source
|
||||
"""
|
||||
self.set_detector_threshold()
|
||||
self.set_acquisition_params()
|
||||
self.parent.cam.trigger_mode.put(TriggerSource.EXT_ENABLE)
|
||||
|
||||
def prepare_data_backend(self) -> None:
|
||||
"""
|
||||
Prepare the detector backend of pilatus for a scan
|
||||
|
||||
A zmq service is running on xbl-daq-34 that is waiting
|
||||
for a zmq message to start the writer for the pilatus_2 x12sa-pd-2
|
||||
|
||||
"""
|
||||
|
||||
self.stop_detector_backend()
|
||||
|
||||
self.parent.filepath.set(
|
||||
self.parent.filewriter.compile_full_filename("pilatus_2.h5")
|
||||
).wait()
|
||||
self.parent.cam.file_path.put("/dev/shm/zmq/")
|
||||
self.parent.cam.file_name.put(
|
||||
f"{self.parent.scaninfo.username}_2_{self.parent.scaninfo.scan_number:05d}"
|
||||
)
|
||||
self.parent.cam.auto_increment.put(1) # auto increment
|
||||
self.parent.cam.file_number.put(0) # first iter
|
||||
self.parent.cam.file_format.put(0) # 0: TIFF
|
||||
self.parent.cam.file_template.put("%s%s_%5.5d.cbf")
|
||||
|
||||
# TODO better to remove hard coded path with link to home directory/pilatus_2
|
||||
basepath = f"/sls/X12SA/data/{self.parent.scaninfo.username}/Data10/pilatus_2/"
|
||||
self.parent.filepath_raw = os.path.join(
|
||||
basepath,
|
||||
self.parent.filewriter.get_scan_directory(self.parent.scaninfo.scan_number, 1000, 5),
|
||||
)
|
||||
# Make directory if needed
|
||||
self.create_directory(self.parent.filepath_raw)
|
||||
|
||||
headers = {"Content-Type": "application/json", "Accept": "application/json"}
|
||||
# start the stream on x12sa-pd-2
|
||||
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
|
||||
data_msg = {
|
||||
"source": [
|
||||
{
|
||||
"searchPath": "/",
|
||||
"searchPattern": "glob:*.cbf",
|
||||
"destinationPath": self.parent.filepath_raw,
|
||||
}
|
||||
]
|
||||
}
|
||||
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
|
||||
logger.info(f"{res.status_code} - {res.text} - {res.content}")
|
||||
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
|
||||
# start the data receiver on xbl-daq-34
|
||||
url = "http://xbl-daq-34:8091/pilatus_2/run"
|
||||
data_msg = [
|
||||
"zmqWriter",
|
||||
self.parent.scaninfo.username,
|
||||
{
|
||||
"addr": "tcp://x12sa-pd-2:8888",
|
||||
"dst": ["file"],
|
||||
"numFrm": int(
|
||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
||||
),
|
||||
"timeout": 2000,
|
||||
"ifType": "PULL",
|
||||
"user": self.parent.scaninfo.username,
|
||||
},
|
||||
]
|
||||
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
|
||||
logger.info(f"{res.status_code} - {res.text} - {res.content}")
|
||||
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
|
||||
# Wait for server to become available again
|
||||
time.sleep(0.1)
|
||||
logger.info(f"{res.status_code} -{res.text} - {res.content}")
|
||||
|
||||
# Send requests.put to xbl-daq-34 to wait for data
|
||||
url = "http://xbl-daq-34:8091/pilatus_2/wait"
|
||||
data_msg = [
|
||||
"zmqWriter",
|
||||
self.parent.scaninfo.username,
|
||||
{
|
||||
"frmCnt": int(
|
||||
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
|
||||
),
|
||||
"timeout": 2000,
|
||||
},
|
||||
]
|
||||
try:
|
||||
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
|
||||
logger.info(f"{res}")
|
||||
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
except Exception as exc:
|
||||
logger.info(f"Pilatus2 wait threw Exception: {exc}")
|
||||
|
||||
def set_detector_threshold(self) -> None:
|
||||
"""
|
||||
Set correct detector threshold to 1/2 of current X-ray energy, allow 5% tolerance
|
||||
|
||||
Threshold might be in ev or keV
|
||||
"""
|
||||
|
||||
# get current beam energy from device manageer
|
||||
mokev = self.parent.device_manager.devices.mokev.obj.read()[
|
||||
self.parent.device_manager.devices.mokev.name
|
||||
]["value"]
|
||||
factor = 1
|
||||
|
||||
# Check if energies are eV or keV, assume keV as the default
|
||||
unit = getattr(self.parent.cam.threshold_energy, "units", None)
|
||||
if unit is not None and unit == "eV":
|
||||
factor = 1000
|
||||
|
||||
# set energy on detector
|
||||
setpoint = int(mokev * factor)
|
||||
|
||||
# set threshold on detector
|
||||
threshold = self.parent.cam.threshold_energy.read()[self.parent.cam.threshold_energy.name][
|
||||
"value"
|
||||
]
|
||||
if not np.isclose(setpoint / 2, threshold, rtol=0.05):
|
||||
self.parent.cam.threshold_energy.set(setpoint / 2)
|
||||
|
||||
def set_acquisition_params(self) -> None:
|
||||
"""Set acquisition parameters for the detector"""
|
||||
|
||||
# Set number of images and frames (frames is for internal burst of detector)
|
||||
self.parent.cam.num_images.put(
|
||||
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
|
||||
)
|
||||
self.parent.cam.num_frames.put(1)
|
||||
|
||||
# Update the readout time of the detector
|
||||
self.update_readout_time()
|
||||
|
||||
def create_directory(self, filepath: str) -> None:
|
||||
"""Create directory if it does not exist"""
|
||||
os.makedirs(filepath, exist_ok=True)
|
||||
|
||||
def close_file_writer(self) -> None:
|
||||
"""
|
||||
Close the file writer for pilatus_2
|
||||
|
||||
Delete the data from x12sa-pd-2
|
||||
|
||||
"""
|
||||
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
|
||||
try:
|
||||
res = self.send_requests_delete(url=url)
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
except Exception as exc:
|
||||
logger.info(f"Pilatus2 close threw Exception: {exc}")
|
||||
|
||||
def stop_file_writer(self) -> None:
|
||||
"""
|
||||
Stop the file writer for pilatus_2
|
||||
|
||||
Runs on xbl-daq-34
|
||||
"""
|
||||
url = "http://xbl-daq-34:8091/pilatus_2/stop"
|
||||
res = self.send_requests_put(url=url)
|
||||
if not res.ok:
|
||||
res.raise_for_status()
|
||||
|
||||
def send_requests_put(self, url: str, data: list = None, headers: dict = None) -> object:
|
||||
"""
|
||||
Send a put request to the given url
|
||||
|
||||
Args:
|
||||
url (str): url to send the request to
|
||||
data (dict): data to be sent with the request (optional)
|
||||
headers (dict): headers to be sent with the request (optional)
|
||||
|
||||
Returns:
|
||||
status code of the request
|
||||
"""
|
||||
return requests.put(url=url, data=json.dumps(data), headers=headers, timeout=5)
|
||||
|
||||
def send_requests_delete(self, url: str, headers: dict = None) -> object:
|
||||
"""
|
||||
Send a delete request to the given url
|
||||
|
||||
Args:
|
||||
url (str): url to send the request to
|
||||
headers (dict): headers to be sent with the request (optional)
|
||||
|
||||
Returns:
|
||||
status code of the request
|
||||
"""
|
||||
return requests.delete(url=url, headers=headers, timeout=5)
|
||||
|
||||
def on_pre_scan(self) -> None:
|
||||
"""Prepare detector for scan"""
|
||||
self.arm_acquisition()
|
||||
|
||||
def arm_acquisition(self) -> None:
|
||||
"""Arms the detector for the acquisition"""
|
||||
self.parent.cam.acquire.put(1)
|
||||
# TODO is this sleep needed? to be tested with detector and for how long
|
||||
time.sleep(0.5)
|
||||
|
||||
def on_unstage(self) -> None:
|
||||
"""Unstage the detector"""
|
||||
pass
|
||||
|
||||
def on_complete(self) -> None:
|
||||
"""Complete the scan"""
|
||||
self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS)
|
||||
self.publish_file_location(
|
||||
done=True, successful=True, metadata={"input_path": self.parent.filepath_raw}
|
||||
)
|
||||
|
||||
def finished(self, timeout: int = 5) -> None:
|
||||
"""Check if acquisition is finished."""
|
||||
# pylint: disable=protected-access
|
||||
# TODO: at the moment this relies on device.mcs.obj._staged attribute
|
||||
signal_conditions = [
|
||||
(lambda: self.parent.device_manager.devices.mcs.obj._staged, Staged.no)
|
||||
]
|
||||
if not self.wait_for_signals(
|
||||
signal_conditions=signal_conditions,
|
||||
timeout=timeout,
|
||||
check_stopped=True,
|
||||
all_signals=True,
|
||||
):
|
||||
raise PilatusTimeoutError(
|
||||
f"Reached timeout with detector state {signal_conditions[0][0]}, std_daq state"
|
||||
f" {signal_conditions[1][0]} and received frames of {signal_conditions[2][0]} for"
|
||||
" the file writer"
|
||||
)
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
|
||||
def on_stop(self) -> None:
|
||||
"""Stop detector"""
|
||||
self.stop_detector()
|
||||
self.stop_detector_backend()
|
||||
|
||||
def stop_detector(self) -> None:
|
||||
"""Stop detector"""
|
||||
self.parent.cam.acquire.put(0)
|
||||
|
||||
def stop_detector_backend(self) -> None:
|
||||
"""Stop the file writer zmq service for pilatus_2"""
|
||||
self.close_file_writer()
|
||||
time.sleep(0.1)
|
||||
self.stop_file_writer()
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
class PilatuscSAXS(PSIDetectorBase):
|
||||
"""Pilatus_2 300k detector for CSAXS
|
||||
|
||||
Parent class: PSIDetectorBase
|
||||
|
||||
class attributes:
|
||||
custom_prepare_cls (Eiger9MSetup) : Custom detector setup class for cSAXS,
|
||||
inherits from CustomDetectorMixin
|
||||
cam (SLSDetectorCam) : Detector camera
|
||||
MIN_READOUT (float) : Minimum readout time for the detector
|
||||
|
||||
"""
|
||||
|
||||
# Specify which functions are revealed to the user in BEC client
|
||||
USER_ACCESS = []
|
||||
|
||||
# specify Setup class
|
||||
custom_prepare_cls = PilatusSetup
|
||||
# specify minimum readout time for detector
|
||||
MIN_READOUT = 3e-3
|
||||
TIMEOUT_FOR_SIGNALS = 5
|
||||
# specify class attributes
|
||||
cam = ADCpt(SLSDetectorCam, "cam1:")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pilatus_2 = PilatuscSAXS(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True)
|
||||
@@ -15,10 +15,10 @@ CI/CD pipelines can run without the pyueye library or the related DLLs installed
|
||||
from __future__ import annotations
|
||||
|
||||
import atexit
|
||||
import time
|
||||
from typing import Literal
|
||||
|
||||
import numpy as np
|
||||
import time
|
||||
from bec_lib.logger import bec_logger
|
||||
|
||||
from csaxs_bec.devices.ids_cameras.base_integration.utils import check_error
|
||||
@@ -67,8 +67,8 @@ class IDSCameraObject:
|
||||
check_error(ueye.is_SetDisplayMode(self.h_cam, ueye.IS_SET_DM_DIB), "IDSCameraObject")
|
||||
|
||||
if (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_BAYER
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_BAYER
|
||||
):
|
||||
logger.info("Bayer color mode detected.")
|
||||
# setup the color depth to the current windows setting
|
||||
@@ -77,16 +77,16 @@ class IDSCameraObject:
|
||||
) # TODO This raises an error - maybe check the m_n_colormode value
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
elif (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_CBYCRY
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_CBYCRY
|
||||
):
|
||||
# for color camera models use RGB32 mode
|
||||
self.m_n_colormode = self.ueye.IS_CM_BGRA8_PACKED
|
||||
self.n_bits_per_pixel = self.ueye.INT(32)
|
||||
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
|
||||
elif (
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_MONOCHROME
|
||||
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
|
||||
== self.ueye.IS_COLORMODE_MONOCHROME
|
||||
):
|
||||
# for color camera models use RGB32 mode
|
||||
self.m_n_colormode = self.ueye.IS_CM_MONO8
|
||||
@@ -160,12 +160,12 @@ class Camera:
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
camera_id: int,
|
||||
m_n_colormode: Literal[0, 1, 2, 3] = 1,
|
||||
bits_per_pixel: int = 24,
|
||||
connect: bool = True,
|
||||
force_monochrome: bool = False,
|
||||
self,
|
||||
camera_id: int,
|
||||
m_n_colormode: Literal[0, 1, 2, 3] = 1,
|
||||
bits_per_pixel: int = 24,
|
||||
connect: bool = True,
|
||||
force_monochrome: bool = False,
|
||||
):
|
||||
self.ueye = ueye
|
||||
self.camera_id = camera_id
|
||||
@@ -173,8 +173,13 @@ class Camera:
|
||||
self.force_monochrome = force_monochrome
|
||||
self._connected = False
|
||||
self.cam = None
|
||||
|
||||
atexit.register(self.on_disconnect)
|
||||
|
||||
self._enable_warning_rate_limit: bool = False
|
||||
self._last_rate_limited_log: float = 0
|
||||
self._warning_log_rate_limit_s: float = 10
|
||||
|
||||
if connect:
|
||||
self.on_connect()
|
||||
|
||||
@@ -255,7 +260,7 @@ class Camera:
|
||||
def get_image_data(self) -> np.ndarray | None:
|
||||
"""Get the image data from the camera."""
|
||||
if not self._connected:
|
||||
logger.warning("Camera is not connected.")
|
||||
self._rate_limited_warning_log("Camera is not connected.")
|
||||
return None
|
||||
array = self.ueye.get_data(
|
||||
self.cam.pc_image_mem,
|
||||
@@ -282,6 +287,22 @@ class Camera:
|
||||
|
||||
return img
|
||||
|
||||
def set_camera_rate_limiting(self, enabled: bool, rate_limit_s: float | None = None):
|
||||
if rate_limit_s is not None:
|
||||
if rate_limit_s <= 0:
|
||||
raise ValueError(f"Invalid rate limit: {rate_limit_s}, must be positive nonzero.")
|
||||
self._warning_log_rate_limit_s = rate_limit_s
|
||||
self._enable_warning_rate_limit = enabled
|
||||
|
||||
def _rate_limited_warning_log(self, msg: "str"):
|
||||
if (
|
||||
self._enable_warning_rate_limit
|
||||
and time.monotonic() < self._last_rate_limited_log + self._warning_log_rate_limit_s
|
||||
):
|
||||
return
|
||||
self._last_rate_limited_log = time.monotonic()
|
||||
logger.warning(msg)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage
|
||||
|
||||
@@ -29,8 +29,14 @@ class IDSCamera(PSIDeviceBase):
|
||||
to interact with the IDS camera using the pyueye library.
|
||||
"""
|
||||
|
||||
image = Cpt(PreviewSignal, name="image", ndim=2, doc="Preview signal for the camera.", num_rotation_90=0,
|
||||
transpose=False)
|
||||
image = Cpt(
|
||||
PreviewSignal,
|
||||
name="image",
|
||||
ndim=2,
|
||||
doc="Preview signal for the camera.",
|
||||
num_rotation_90=0,
|
||||
transpose=False,
|
||||
)
|
||||
roi_signal = Cpt(
|
||||
AsyncSignal,
|
||||
name="roi_signal",
|
||||
@@ -43,19 +49,19 @@ class IDSCamera(PSIDeviceBase):
|
||||
USER_ACCESS = ["live_mode", "mask", "set_rect_roi", "get_last_image"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
camera_id: int,
|
||||
prefix: str = "",
|
||||
scan_info: ScanInfo | None = None,
|
||||
m_n_colormode: Literal[0, 1, 2, 3] = 1,
|
||||
bits_per_pixel: Literal[8, 24] = 24,
|
||||
live_mode: bool = False,
|
||||
num_rotation_90: int = 0,
|
||||
transpose: bool = False,
|
||||
force_monochrome: bool = False,
|
||||
**kwargs,
|
||||
self,
|
||||
*,
|
||||
name: str,
|
||||
camera_id: int,
|
||||
prefix: str = "",
|
||||
scan_info: ScanInfo | None = None,
|
||||
m_n_colormode: Literal[0, 1, 2, 3] = 1,
|
||||
bits_per_pixel: Literal[8, 24] = 24,
|
||||
live_mode: bool = False,
|
||||
num_rotation_90: int = 0,
|
||||
transpose: bool = False,
|
||||
force_monochrome: bool = False,
|
||||
**kwargs,
|
||||
):
|
||||
"""Initialize the IDS Camera.
|
||||
|
||||
@@ -133,7 +139,7 @@ class IDSCamera(PSIDeviceBase):
|
||||
if x + width > img_shape[1] or y + height > img_shape[0]:
|
||||
raise ValueError("ROI exceeds camera dimensions.")
|
||||
mask = np.zeros(img_shape, dtype=np.uint8)
|
||||
mask[y: y + height, x: x + width] = 1
|
||||
mask[y : y + height, x : x + width] = 1
|
||||
self.mask = mask
|
||||
|
||||
def _start_live(self):
|
||||
@@ -162,6 +168,7 @@ class IDSCamera(PSIDeviceBase):
|
||||
|
||||
def _live_mode_loop(self, stop_event: threading.Event):
|
||||
"""Loop to capture images in live mode."""
|
||||
self.cam.set_camera_rate_limiting(True)
|
||||
while not stop_event.is_set():
|
||||
try:
|
||||
self.process_data(self.cam.get_image_data())
|
||||
@@ -169,6 +176,7 @@ class IDSCamera(PSIDeviceBase):
|
||||
logger.error(f"Error in live mode loop: {e}")
|
||||
break
|
||||
stop_event.wait(0.2) # 5 Hz
|
||||
self.cam.set_camera_rate_limiting(False)
|
||||
|
||||
def process_data(self, image: np.ndarray | None):
|
||||
"""Process the image data before sending it to the preview signal."""
|
||||
|
||||
@@ -442,6 +442,14 @@ class NPointAxis(Device, PositionerBase):
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
def wait_for_connection(self, timeout: float = 30.0) -> bool:
|
||||
self.controller.on(timeout=timeout)
|
||||
|
||||
def destroy(self):
|
||||
"""Make sure to turn off the controller socket on destroy."""
|
||||
self.controller.off(update_config=False)
|
||||
return super().destroy()
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@@ -212,10 +212,14 @@ class FlomniGalilMotor(Device, PositionerBase):
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
def wait_for_connection(self, timeout: int = 10, **kwargs) -> None:
|
||||
"""Wait for the device to be connected."""
|
||||
def wait_for_connection(self, timeout: float = 30.0) -> bool:
|
||||
self.controller.on(timeout=timeout)
|
||||
|
||||
def destroy(self):
|
||||
"""Make sure to turn off the controller socket on destroy."""
|
||||
self.controller.off(update_config=False)
|
||||
return super().destroy()
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@@ -185,10 +185,14 @@ class FuprGalilMotor(Device, PositionerBase):
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
def wait_for_connection(self, timeout: int = 10, **kwargs) -> None:
|
||||
"""Wait for the device to be connected."""
|
||||
def wait_for_connection(self, timeout: float = 30.0) -> bool:
|
||||
self.controller.on(timeout=timeout)
|
||||
|
||||
def destroy(self):
|
||||
"""Make sure to turn off the controller socket on destroy."""
|
||||
self.controller.off(update_config=False)
|
||||
return super().destroy()
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@@ -59,12 +59,12 @@ class GalilController(Controller):
|
||||
"all_axes_referenced",
|
||||
]
|
||||
|
||||
OKBLUE = '\033[94m'
|
||||
OKCYAN = '\033[96m'
|
||||
OKGREEN = '\033[92m'
|
||||
WARNING = '\033[93m'
|
||||
FAIL = '\033[91m'
|
||||
ENDC = '\033[0m'
|
||||
OKBLUE = "\033[94m"
|
||||
OKCYAN = "\033[96m"
|
||||
OKGREEN = "\033[92m"
|
||||
WARNING = "\033[93m"
|
||||
FAIL = "\033[91m"
|
||||
ENDC = "\033[0m"
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str) -> None:
|
||||
@@ -115,29 +115,29 @@ class GalilController(Controller):
|
||||
|
||||
def axis_is_referenced(self, axis_Id_numeric) -> bool:
|
||||
return bool(float(self.socket_put_and_receive(f"MG axisref[{axis_Id_numeric}]").strip()))
|
||||
|
||||
|
||||
def folerr_status(self, axis_Id_numeric) -> bool:
|
||||
return bool(float(self.socket_put_and_receive(f"MG folaxerr[{axis_Id_numeric}]").strip()))
|
||||
|
||||
def motor_temperature(self, axis_Id_numeric) -> float:
|
||||
#this is only valid for omny. consider moving to ogalil
|
||||
# this is only valid for omny. consider moving to ogalil
|
||||
voltage = float(self.socket_put_and_receive(f"MG @AN[{axis_Id_numeric+1}]").strip())
|
||||
voltage2 = float(self.socket_put_and_receive(f"MG @AN[{axis_Id_numeric+1}]").strip())
|
||||
if voltage2 < voltage:
|
||||
voltage = voltage2
|
||||
# convert from [-10,10]V to [0,300]degC
|
||||
temperature_degC = round((voltage+10.0) / 20.0 * 300.0, 1)
|
||||
temperature_degC = round((voltage + 10.0) / 20.0 * 300.0, 1)
|
||||
|
||||
#the motors of the parking station have a different offset
|
||||
#the range is reduced, so if at the limit, we show an extreme value
|
||||
# the motors of the parking station have a different offset
|
||||
# the range is reduced, so if at the limit, we show an extreme value
|
||||
if self.sock.port == 8082:
|
||||
#controller 2
|
||||
# controller 2
|
||||
if axis_Id_numeric == 6:
|
||||
temperature_degC = round((voltage+10.0-11.4) / 20.0 * 300.0, 1)
|
||||
temperature_degC = round((voltage + 10.0 - 11.4) / 20.0 * 300.0, 1)
|
||||
if voltage > 9.9:
|
||||
temperature_degC = 300
|
||||
if axis_Id_numeric == 7:
|
||||
temperature_degC = round((voltage+.0-12) / 20.0 * 300.0, 1)
|
||||
temperature_degC = round((voltage + 0.0 - 12) / 20.0 * 300.0, 1)
|
||||
if voltage > 9.9:
|
||||
temperature_degC = 300
|
||||
return temperature_degC
|
||||
@@ -147,16 +147,15 @@ class GalilController(Controller):
|
||||
Check if all axes are referenced.
|
||||
"""
|
||||
return bool(float(self.socket_put_and_receive("MG allaxref").strip()))
|
||||
|
||||
def _omny_get_microstep_position(self,axis_Id):
|
||||
|
||||
def _omny_get_microstep_position(self, axis_Id):
|
||||
return float(self.socket_put_and_receive(f"MG _TD{axis_Id}").strip())
|
||||
|
||||
|
||||
def _omny_get_reference_limit(self,axis_Id):
|
||||
|
||||
def _omny_get_reference_limit(self, axis_Id):
|
||||
get_axis_no = float(self.socket_put_and_receive(f"MG frmmv").strip())
|
||||
if(get_axis_no>0):
|
||||
if get_axis_no > 0:
|
||||
reference_is_before = float(self.socket_put_and_receive(f"MG _FL{axis_Id}").strip())
|
||||
elif(get_axis_no<0):
|
||||
elif get_axis_no < 0:
|
||||
reference_is_before = float(self.socket_put_and_receive(f"MG _BL{axis_Id}").strip())
|
||||
else:
|
||||
reference_is_before = 0
|
||||
@@ -187,7 +186,11 @@ class GalilController(Controller):
|
||||
while self.is_axis_moving(None, axis_Id_numeric):
|
||||
time.sleep(0.01)
|
||||
if verbose:
|
||||
self.get_device_manager().connector.send_client_info(f"Current microstep position {self._omny_get_microstep_position(axis_Id):.0f}", scope="drive axis to limit", show_asap=True)
|
||||
self.device_manager.connector.send_client_info(
|
||||
f"Current microstep position {self._omny_get_microstep_position(axis_Id):.0f}",
|
||||
scope="drive axis to limit",
|
||||
show_asap=True,
|
||||
)
|
||||
time.sleep(0.5)
|
||||
|
||||
# check if we actually hit the limit
|
||||
@@ -201,13 +204,7 @@ class GalilController(Controller):
|
||||
else:
|
||||
print("Limit reached.")
|
||||
|
||||
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 find_reference(self, axis_Id_numeric: int, verbose=0, raise_error = 1) -> None:
|
||||
def find_reference(self, axis_Id_numeric: int, verbose=0, raise_error=1) -> None:
|
||||
"""
|
||||
Find the reference of an axis.
|
||||
|
||||
@@ -224,7 +221,11 @@ class GalilController(Controller):
|
||||
while self.is_axis_moving(None, axis_Id_numeric):
|
||||
time.sleep(0.1)
|
||||
if verbose:
|
||||
self.get_device_manager().connector.send_client_info(f"Current microstep position {self._omny_get_microstep_position(axis_Id):.0f} reference is before {self._omny_get_reference_limit(axis_Id)}", scope="find axis reference", show_asap=True)
|
||||
self.device_manager.connector.send_client_info(
|
||||
f"Current microstep position {self._omny_get_microstep_position(axis_Id):.0f} reference is before {self._omny_get_reference_limit(axis_Id)}",
|
||||
scope="find axis reference",
|
||||
show_asap=True,
|
||||
)
|
||||
time.sleep(0.5)
|
||||
|
||||
if not self.axis_is_referenced(axis_Id_numeric):
|
||||
@@ -236,7 +237,6 @@ class GalilController(Controller):
|
||||
logger.info(f"Successfully found reference of axis {axis_Id_numeric}.")
|
||||
print(f"Successfully found reference of axis {axis_Id_numeric}.")
|
||||
|
||||
|
||||
def show_running_threads(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"Threads on {self.sock.host}:{self.sock.port}"
|
||||
@@ -251,7 +251,7 @@ class GalilController(Controller):
|
||||
|
||||
def is_motor_on(self, axis_Id) -> bool:
|
||||
return not bool(float(self.socket_put_and_receive(f"MG _MO{axis_Id}").strip()))
|
||||
|
||||
|
||||
def get_motor_limit_switch(self, axis_Id) -> list:
|
||||
"""
|
||||
Get the status of the motor limit switches.
|
||||
@@ -269,14 +269,7 @@ class GalilController(Controller):
|
||||
def describe(self) -> None:
|
||||
t = PrettyTable()
|
||||
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
|
||||
field_names = [
|
||||
"Axis",
|
||||
"Name",
|
||||
"Referenced",
|
||||
"Motor On",
|
||||
"Limits",
|
||||
"Position",
|
||||
]
|
||||
field_names = ["Axis", "Name", "Referenced", "Motor On", "Limits", "Position"]
|
||||
# in case of OMNY
|
||||
if self.sock.host == "mpc3217.psi.ch":
|
||||
field_names.append("Temperature")
|
||||
@@ -286,7 +279,7 @@ class GalilController(Controller):
|
||||
axis = self._axis[ax]
|
||||
if axis is not None:
|
||||
if self.sock.host == "mpc3217.psi.ch":
|
||||
#case of omny. possibly consider moving to ogalil
|
||||
# case of omny. possibly consider moving to ogalil
|
||||
motor_on = self.is_motor_on(axis.axis_Id)
|
||||
if motor_on == True:
|
||||
motor_on = self.WARNING + "ON" + self.ENDC
|
||||
@@ -299,7 +292,7 @@ class GalilController(Controller):
|
||||
else:
|
||||
folerr_status = "False"
|
||||
position = axis.readback.read().get(axis.name).get("value")
|
||||
position = f'{position:.3f}'
|
||||
position = f"{position:.3f}"
|
||||
t.add_row(
|
||||
[
|
||||
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
|
||||
@@ -330,8 +323,6 @@ class GalilController(Controller):
|
||||
self.show_running_threads()
|
||||
self.show_status_other()
|
||||
|
||||
|
||||
|
||||
def show_status_other(self) -> None:
|
||||
"""
|
||||
Show additional device-specific status information.
|
||||
@@ -420,7 +411,7 @@ class GalilSetpointSignal(GalilSignalBase):
|
||||
while self.controller.is_thread_active(0):
|
||||
time.sleep(0.1)
|
||||
|
||||
#in the case of lamni, consider moving to lgalil
|
||||
# in the case of lamni, consider moving to lgalil
|
||||
if self.parent.axis_Id_numeric == 2 and self.controller.sock.host == "mpc2680.psi.ch":
|
||||
try:
|
||||
rt = self.parent.device_manager.devices[self.parent.rt]
|
||||
|
||||
35
csaxs_bec/devices/omny/galil/galil_rio.py
Normal file
35
csaxs_bec/devices/omny/galil/galil_rio.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketSignal
|
||||
|
||||
from csaxs_bec.devices.omny.galil.galil_ophyd import GalilCommunicationError, retry_once
|
||||
|
||||
|
||||
class GalilRIO(Controller):
|
||||
|
||||
@threadlocked
|
||||
def socket_put(self, val: str) -> None:
|
||||
self.sock.put(f"{val}\r".encode())
|
||||
|
||||
@retry_once
|
||||
def socket_put_confirmed(self, val: str) -> None:
|
||||
"""Send message to controller and ensure that it is received by checking that the socket receives a colon.
|
||||
|
||||
Args:
|
||||
val (str): Message that should be sent to the socket
|
||||
|
||||
Raises:
|
||||
GalilCommunicationError: Raised if the return value is not a colon.
|
||||
|
||||
"""
|
||||
return_val = self.socket_put_and_receive(val)
|
||||
if return_val != ":":
|
||||
raise GalilCommunicationError(
|
||||
f"Expected return value of ':' but instead received {return_val}"
|
||||
)
|
||||
|
||||
|
||||
class GalilRIOSignalBase(SocketSignal):
|
||||
def __init__(self, signal_name, **kwargs):
|
||||
self.signal_name = signal_name
|
||||
super().__init__(**kwargs)
|
||||
self.rio_controller = self.parent.rio_controller
|
||||
@@ -170,10 +170,14 @@ class LamniGalilMotor(Device, PositionerBase):
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
def wait_for_connection(self, timeout: int = 10, **kwargs) -> None:
|
||||
"""Wait for the device to be connected."""
|
||||
def wait_for_connection(self, timeout: float = 30.0) -> bool:
|
||||
self.controller.on(timeout=timeout)
|
||||
|
||||
def destroy(self):
|
||||
"""Make sure to turn off the controller socket on destroy."""
|
||||
self.controller.off(update_config=False)
|
||||
return super().destroy()
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@@ -324,10 +324,14 @@ class OMNYGalilMotor(Device, PositionerBase):
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
def wait_for_connection(self, timeout: int = 10, **kwargs) -> None:
|
||||
"""Wait for the device to be connected."""
|
||||
def wait_for_connection(self, timeout: float = 30.0) -> bool:
|
||||
self.controller.on(timeout=timeout)
|
||||
|
||||
def destroy(self):
|
||||
"""Make sure to turn off the controller socket on destroy."""
|
||||
self.controller.off(update_config=False)
|
||||
return super().destroy()
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@@ -530,10 +530,14 @@ class SGalilMotor(Device, PositionerBase):
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
def wait_for_connection(self, timeout: int = 10, **kwargs) -> None:
|
||||
"""Wait for the device to be connected."""
|
||||
def wait_for_connection(self, timeout: float = 30.0) -> bool:
|
||||
self.controller.on(timeout=timeout)
|
||||
|
||||
def destroy(self):
|
||||
"""Make sure to turn off the controller socket on destroy."""
|
||||
self.controller.off(update_config=False)
|
||||
return super().destroy()
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import threading
|
||||
import time
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
from typing import List
|
||||
|
||||
import numpy as np
|
||||
from bec_lib.logger import bec_logger
|
||||
from bec_lib import bec_logger, messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from ophyd import Component as Cpt
|
||||
from ophyd import Device, DeviceStatus, PositionerBase, Signal
|
||||
from ophyd import Device, PositionerBase, Signal
|
||||
from ophyd.status import wait as status_wait
|
||||
from ophyd.utils import LimitError
|
||||
from ophyd_devices.utils.bec_signals import AsyncMultiSignal, ProgressSignal
|
||||
from ophyd_devices.utils.controller import Controller, threadlocked
|
||||
from ophyd_devices.utils.socket import SocketIO, raise_if_disconnected
|
||||
from prettytable import PrettyTable
|
||||
@@ -25,9 +23,6 @@ from csaxs_bec.devices.omny.rt.rt_ophyd import (
|
||||
retry_once,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from bec_server.device_server.devices.devicemanager import DeviceManagerDS
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
@@ -133,15 +128,15 @@ class RtFlomniController(Controller):
|
||||
while not self.slew_rate_limiters_on_target() or np.abs(self.pid_y()) > 0.1:
|
||||
time.sleep(0.05)
|
||||
|
||||
self.get_device_manager().devices.rty.update_user_parameter({"tomo_additional_offsety": 0})
|
||||
self.device_manager.devices.rty.update_user_parameter({"tomo_additional_offsety": 0})
|
||||
self.clear_trajectory_generator()
|
||||
|
||||
self.laser_tracker_on()
|
||||
|
||||
# move to 0. FUPR will set the rotation angle during readout
|
||||
self.get_device_manager().devices.fsamroy.obj.move(0, wait=True)
|
||||
self.device_manager.devices.fsamroy.obj.move(0, wait=True)
|
||||
|
||||
fsamx = self.get_device_manager().devices.fsamx
|
||||
fsamx = self.device_manager.devices.fsamx
|
||||
|
||||
fsamx.obj.pid_x_correction = 0
|
||||
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]")
|
||||
@@ -171,7 +166,7 @@ class RtFlomniController(Controller):
|
||||
self.show_cyclic_error_compensation()
|
||||
|
||||
self.rt_pid_voltage = self.get_pid_x()
|
||||
rtx = self.get_device_manager().devices.rtx
|
||||
rtx = self.device_manager.devices.rtx
|
||||
rtx.update_user_parameter({"rt_pid_voltage": self.rt_pid_voltage})
|
||||
|
||||
self.set_device_read_write("fsamx", False)
|
||||
@@ -182,7 +177,7 @@ class RtFlomniController(Controller):
|
||||
def move_samx_to_scan_region(self, fovx: float, cenx: float):
|
||||
time.sleep(0.05)
|
||||
if self.rt_pid_voltage is None:
|
||||
rtx = self.get_device_manager().devices.rtx
|
||||
rtx = self.device_manager.devices.rtx
|
||||
self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage")
|
||||
if self.rt_pid_voltage is None:
|
||||
raise RtError(
|
||||
@@ -199,7 +194,7 @@ class RtFlomniController(Controller):
|
||||
break
|
||||
wait_on_exit = True
|
||||
self.socket_put("v0")
|
||||
fsamx = self.get_device_manager().devices.fsamx
|
||||
fsamx = self.device_manager.devices.fsamx
|
||||
fsamx.read_only = False
|
||||
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]")
|
||||
fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.007
|
||||
@@ -245,7 +240,7 @@ class RtFlomniController(Controller):
|
||||
self.set_device_read_write("foptx", True)
|
||||
self.set_device_read_write("fopty", True)
|
||||
|
||||
fsamx = self.get_device_manager().devices.fsamx
|
||||
fsamx = self.device_manager.devices.fsamx
|
||||
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=025*stppermm[4]")
|
||||
print("rt feedback is now disalbed.")
|
||||
|
||||
@@ -275,7 +270,8 @@ class RtFlomniController(Controller):
|
||||
self.laser_update_tracker_info()
|
||||
if self.tracker_info["enabled_z"] and self.tracker_info["enabled_y"]:
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
def laser_tracker_on(self):
|
||||
if not self.laser_tracker_check_enabled():
|
||||
@@ -295,12 +291,8 @@ class RtFlomniController(Controller):
|
||||
self.socket_put("T1")
|
||||
time.sleep(0.5)
|
||||
|
||||
self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed(
|
||||
"trackyct=0"
|
||||
)
|
||||
self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed(
|
||||
"trackzct=0"
|
||||
)
|
||||
self.device_manager.devices.ftrackz.obj.controller.socket_put_confirmed("trackyct=0")
|
||||
self.device_manager.devices.ftrackz.obj.controller.socket_put_confirmed("trackzct=0")
|
||||
|
||||
self.laser_tracker_wait_on_target()
|
||||
logger.info("Laser tracker running!")
|
||||
@@ -347,7 +339,7 @@ class RtFlomniController(Controller):
|
||||
}
|
||||
|
||||
def laser_tracker_galil_enable(self):
|
||||
ftrackz_con = self.get_device_manager().devices.ftrackz.obj.controller
|
||||
ftrackz_con = self.device_manager.devices.ftrackz.obj.controller
|
||||
ftrackz_con.socket_put_confirmed("tracken=1")
|
||||
ftrackz_con.socket_put_confirmed("trackyct=0")
|
||||
ftrackz_con.socket_put_confirmed("trackzct=0")
|
||||
@@ -387,61 +379,66 @@ class RtFlomniController(Controller):
|
||||
val = float(self.socket_put_and_receive(f"j{axis_number}").strip())
|
||||
return val
|
||||
|
||||
def laser_tracker_check_signalstrength(self) -> Literal["ok", "low", "toolow", "disabled"]:
|
||||
"""
|
||||
Check the signal strength of the laser tracker interferometer.
|
||||
Returns:
|
||||
str: "ok" if signal is above low limit, "low" if signal is below low limit but above min limit,
|
||||
"toolow" if signal is below min limit, "disabled" if tracker is not enabled.
|
||||
"""
|
||||
def laser_tracker_check_signalstrength(self):
|
||||
if not self.laser_tracker_check_enabled():
|
||||
return "disabled"
|
||||
returnval = "disabled"
|
||||
else:
|
||||
returnval = "ok"
|
||||
self.laser_tracker_wait_on_target()
|
||||
|
||||
returnval = "ok"
|
||||
self.laser_tracker_wait_on_target()
|
||||
|
||||
signal = self.read_ssi_interferometer(1)
|
||||
rtx = self.get_device_manager().devices.rtx
|
||||
min_signal = rtx.user_parameter.get("min_signal")
|
||||
low_signal = rtx.user_parameter.get("low_signal")
|
||||
print(f"low signal: {low_signal}")
|
||||
print(f"min signal: {min_signal}")
|
||||
print(f"signal: {signal}")
|
||||
if signal < min_signal:
|
||||
time.sleep(1)
|
||||
signal = self.read_ssi_interferometer(1)
|
||||
rtx = self.device_manager.devices.rtx
|
||||
min_signal = rtx.user_parameter.get("min_signal")
|
||||
low_signal = rtx.user_parameter.get("low_signal")
|
||||
print(f"low signal: {low_signal}")
|
||||
print(f"min signal: {min_signal}")
|
||||
print(f"signal: {signal}")
|
||||
if signal < min_signal:
|
||||
time.sleep(1)
|
||||
if signal < min_signal:
|
||||
print(
|
||||
f"\x1b[91mThe signal of the tracker {signal} is below the minimum required signal of {min_signal}. Readjustment requred!\x1b[0m"
|
||||
)
|
||||
returnval = "toolow"
|
||||
# raise RtError("The interferometer signal of tracker is too low.")
|
||||
elif signal < low_signal:
|
||||
print(
|
||||
f"\x1b[91mThe signal of the tracker {signal} is below the minimum required signal of {min_signal}. Readjustment requred!\x1b[0m"
|
||||
f"\x1b[91mThe signal of the tracker {signal} is below the warning limit of {low_signal}. Readjustment recommended!\x1b[0m"
|
||||
)
|
||||
returnval = "toolow"
|
||||
# raise RtError("The interferometer signal of tracker is too low.")
|
||||
elif signal < low_signal:
|
||||
print(
|
||||
f"\x1b[91mThe signal of the tracker {signal} is below the warning limit of {low_signal}. Readjustment recommended!\x1b[0m"
|
||||
)
|
||||
returnval = "low"
|
||||
returnval = "low"
|
||||
return returnval
|
||||
|
||||
def show_signal_strength_interferometer(self):
|
||||
"""
|
||||
Show the signal strength of all four axes of the interferometer.
|
||||
"""
|
||||
t = PrettyTable()
|
||||
t.title = "Interferometer signal strength"
|
||||
t.title = f"Interferometer signal strength"
|
||||
t.field_names = ["Axis", "Value"]
|
||||
for i in range(4):
|
||||
t.add_row([i, self.read_ssi_interferometer(i)])
|
||||
print(t)
|
||||
|
||||
def _get_signals_from_table(self, return_table) -> dict:
|
||||
self.average_stdeviations_x_st_fzp += float(return_table[4])
|
||||
self.average_stdeviations_y_st_fzp += float(return_table[7])
|
||||
signals = {
|
||||
"target_x": {"value": float(return_table[2])},
|
||||
"average_x_st_fzp": {"value": float(return_table[3])},
|
||||
"stdev_x_st_fzp": {"value": float(return_table[4])},
|
||||
"target_y": {"value": float(return_table[5])},
|
||||
"average_y_st_fzp": {"value": float(return_table[6])},
|
||||
"stdev_y_st_fzp": {"value": float(return_table[7])},
|
||||
"average_rotz": {"value": float(return_table[8])},
|
||||
"stdev_rotz": {"value": float(return_table[9])},
|
||||
"average_stdeviations_x_st_fzp": {
|
||||
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
"average_stdeviations_y_st_fzp": {
|
||||
"value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
}
|
||||
return signals
|
||||
|
||||
@threadlocked
|
||||
def start_scan(self):
|
||||
"""
|
||||
Start the scan. Before starting the scan, it is checked that the feedback loop is running.
|
||||
Furthermore, it is checked that at least one target position is planned.
|
||||
|
||||
Raises:
|
||||
RtError: Raised if feedback loop is not running or if no target positions are planned.
|
||||
"""
|
||||
if not self.feedback_is_running():
|
||||
logger.error(
|
||||
"Cannot start scan because feedback loop is not running or there is an"
|
||||
@@ -452,23 +449,18 @@ class RtFlomniController(Controller):
|
||||
" interferometer error."
|
||||
)
|
||||
# here exception
|
||||
(_mode, number_of_positions_planned, _current_position_in_scan) = self.get_scan_status()
|
||||
(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 RtError("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")
|
||||
|
||||
@retry_once
|
||||
@threadlocked
|
||||
def get_scan_status(self) -> tuple[int, int, int]:
|
||||
"""
|
||||
Get the current scan status of the controller.
|
||||
|
||||
Returns:
|
||||
tuple: A tuple containing the mode, number of positions planned, and current position in scan.
|
||||
"""
|
||||
def get_scan_status(self):
|
||||
return_table = (self.socket_put_and_receive("sr")).split(",")
|
||||
if len(return_table) != 3:
|
||||
raise RtCommunicationError(
|
||||
@@ -484,14 +476,87 @@ class RtFlomniController(Controller):
|
||||
current_position_in_scan = int(float(return_table[2]))
|
||||
return (mode, number_of_positions_planned, current_position_in_scan)
|
||||
|
||||
def get_device_manager(self) -> DeviceManagerDS:
|
||||
"""
|
||||
Helper function to get the device manager from one of the axes.
|
||||
"""
|
||||
for axis in self._axis:
|
||||
if hasattr(axis, "device_manager") and axis.device_manager: # type: ignore
|
||||
return axis.device_manager # type: ignore
|
||||
raise BECConfigError("Could not access the device_manager")
|
||||
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
|
||||
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.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
|
||||
self.device_manager.connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=1, metadata=self.readout_metadata
|
||||
),
|
||||
)
|
||||
# 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(0.01)
|
||||
if current_position_in_scan > 5:
|
||||
while current_position_in_scan > read_counter + 1:
|
||||
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
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
time.sleep(0.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
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
self.device_manager.connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=0, metadata=self.readout_metadata
|
||||
),
|
||||
)
|
||||
|
||||
logger.info(
|
||||
"Flomni statistics: Average of all standard deviations: x"
|
||||
f" {self.average_stdeviations_x_st_fzp/read_counter*1000:.1f}, y"
|
||||
f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}"
|
||||
)
|
||||
|
||||
def publish_device_data(self, signals, point_id):
|
||||
self.device_manager.connector.set_and_publish(
|
||||
MessageEndpoints.device_read("rt_flomni"),
|
||||
messages.DeviceMessage(
|
||||
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
|
||||
),
|
||||
)
|
||||
|
||||
def start_readout(self):
|
||||
readout = threading.Thread(target=self.read_positions_from_sampler)
|
||||
readout.start()
|
||||
|
||||
def kickoff(self, metadata):
|
||||
self.readout_metadata = metadata
|
||||
while not self._min_scan_buffer_reached:
|
||||
time.sleep(0.001)
|
||||
self.start_scan()
|
||||
time.sleep(0.1)
|
||||
self.start_readout()
|
||||
|
||||
|
||||
class RtFlomniReadbackSignal(RtReadbackSignal):
|
||||
@@ -613,10 +678,14 @@ class RtFlomniMotor(Device, PositionerBase):
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
def wait_for_connection(self, timeout: int = 10, **kwargs) -> None:
|
||||
"""Wait for the device to be connected."""
|
||||
def wait_for_connection(self, timeout: float = 30.0) -> bool:
|
||||
self.controller.on(timeout=timeout)
|
||||
|
||||
def destroy(self):
|
||||
"""Make sure to turn off the controller socket on destroy."""
|
||||
self.controller.off(update_config=False)
|
||||
return super().destroy()
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
@@ -728,6 +797,9 @@ class RtFlomniMotor(Device, PositionerBase):
|
||||
else:
|
||||
raise TypeError(f"Expected value of type int but received {type(val)}")
|
||||
|
||||
def kickoff(self, metadata, **kwargs) -> None:
|
||||
self.controller.kickoff(metadata)
|
||||
|
||||
@property
|
||||
def egu(self):
|
||||
"""The engineering units (EGU) for positions"""
|
||||
@@ -739,178 +811,6 @@ class RtFlomniMotor(Device, PositionerBase):
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
class RtFlomniFlyer(Device):
|
||||
USER_ACCESS = ["controller"]
|
||||
data = Cpt(
|
||||
AsyncMultiSignal,
|
||||
name="data",
|
||||
signals=[
|
||||
"target_x",
|
||||
"average_x_st_fzp",
|
||||
"stdev_x_st_fzp",
|
||||
"target_y",
|
||||
"average_y_st_fzp",
|
||||
"stdev_y_st_fzp",
|
||||
"average_rotz",
|
||||
"stdev_rotz",
|
||||
"average_stdeviations_x_st_fzp",
|
||||
"average_stdeviations_y_st_fzp",
|
||||
],
|
||||
ndim=0,
|
||||
max_size=10000,
|
||||
async_update={"type": "add", "max_shape": [None]},
|
||||
)
|
||||
progress = Cpt(ProgressSignal, name="progress")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
prefix="",
|
||||
*,
|
||||
name,
|
||||
kind=None,
|
||||
read_attrs=None,
|
||||
configuration_attrs=None,
|
||||
parent=None,
|
||||
host="mpc2844.psi.ch",
|
||||
port=2222,
|
||||
socket_cls=SocketIO,
|
||||
device_manager=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
prefix=prefix,
|
||||
name=name,
|
||||
kind=kind,
|
||||
read_attrs=read_attrs,
|
||||
configuration_attrs=configuration_attrs,
|
||||
parent=parent,
|
||||
**kwargs,
|
||||
)
|
||||
self.shutdown_event = threading.Event()
|
||||
self.controller = RtFlomniController(
|
||||
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
|
||||
)
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
|
||||
def read_positions_from_sampler(self, status: DeviceStatus):
|
||||
"""
|
||||
Read the positions from the sampler and update the data signal.
|
||||
This function runs in a separate thread and continuously checks the
|
||||
scan status.
|
||||
|
||||
Args:
|
||||
status (DeviceStatus): The status object to update when the readout is complete.
|
||||
"""
|
||||
read_counter = 0
|
||||
self.average_stdeviations_x_st_fzp = 0
|
||||
self.average_stdeviations_y_st_fzp = 0
|
||||
self.average_lamni_angle = 0
|
||||
|
||||
mode, number_of_positions_planned, current_position_in_scan = (
|
||||
self.controller.get_scan_status()
|
||||
)
|
||||
|
||||
# while scan is running
|
||||
while mode > 0 and not self.shutdown_event.wait(0.01):
|
||||
# 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.controller.get_scan_status()
|
||||
)
|
||||
if current_position_in_scan > 5:
|
||||
while current_position_in_scan > read_counter + 1:
|
||||
return_table = (
|
||||
self.controller.socket_put_and_receive(f"r{read_counter}")
|
||||
).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
read_counter = read_counter + 1
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.data.set(signals)
|
||||
self.progress.put(
|
||||
value=read_counter,
|
||||
max_value=number_of_positions_planned,
|
||||
done=(number_of_positions_planned == read_counter),
|
||||
)
|
||||
logger.info(f"Updating progress: {read_counter} / {number_of_positions_planned}")
|
||||
|
||||
|
||||
if self.shutdown_event.wait(0.05):
|
||||
logger.info("Shutdown event set, stopping readout.")
|
||||
# if we are here, the shutdown_event is set. We can exit the readout loop.
|
||||
status.set_finished()
|
||||
return
|
||||
|
||||
# read the last samples even though scan is finished already
|
||||
while number_of_positions_planned > read_counter and not self.shutdown_event.is_set():
|
||||
return_table = (self.controller.socket_put_and_receive(f"r{read_counter}")).split(",")
|
||||
logger.info(f"Read {read_counter} out of {number_of_positions_planned}")
|
||||
read_counter = read_counter + 1
|
||||
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
logger.info(f"Setting data: {signals}")
|
||||
self.data.set(signals)
|
||||
self.progress.put(
|
||||
value=read_counter,
|
||||
max_value=number_of_positions_planned,
|
||||
done=(number_of_positions_planned == read_counter),
|
||||
)
|
||||
logger.info(f"Updating progress: {read_counter} / {number_of_positions_planned}")
|
||||
|
||||
|
||||
# NOTE: No need to set the status to failed if the shutdown_event is set.
|
||||
# The stop() method will take care of that.
|
||||
status.set_finished()
|
||||
|
||||
if read_counter != 0:
|
||||
logger.info(
|
||||
"Flomni statistics: Average of all standard deviations: x"
|
||||
f" {self.average_stdeviations_x_st_fzp/read_counter*1000:.1f}, y"
|
||||
f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}"
|
||||
)
|
||||
|
||||
|
||||
def _get_signals_from_table(self, return_table) -> dict:
|
||||
self.average_stdeviations_x_st_fzp += float(return_table[4])
|
||||
self.average_stdeviations_y_st_fzp += float(return_table[7])
|
||||
signals = {
|
||||
f"{self.name}_data_target_x": {"value": float(return_table[2])},
|
||||
f"{self.name}_data_average_x_st_fzp": {"value": float(return_table[3])},
|
||||
f"{self.name}_data_stdev_x_st_fzp": {"value": float(return_table[4])},
|
||||
f"{self.name}_data_target_y": {"value": float(return_table[5])},
|
||||
f"{self.name}_data_average_y_st_fzp": {"value": float(return_table[6])},
|
||||
f"{self.name}_data_stdev_y_st_fzp": {"value": float(return_table[7])},
|
||||
f"{self.name}_data_average_rotz": {"value": float(return_table[8])},
|
||||
f"{self.name}_data_stdev_rotz": {"value": float(return_table[9])},
|
||||
f"{self.name}_data_average_stdeviations_x_st_fzp": {
|
||||
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
f"{self.name}_data_average_stdeviations_y_st_fzp": {
|
||||
"value": self.average_stdeviations_y_st_fzp / (int(return_table[0]) + 1)
|
||||
},
|
||||
}
|
||||
return signals
|
||||
|
||||
def start_readout(self, status: DeviceStatus):
|
||||
logger.info("Starting readout thread.")
|
||||
readout = threading.Thread(target=self.read_positions_from_sampler, args=(status,))
|
||||
readout.start()
|
||||
|
||||
def kickoff(self) -> DeviceStatus:
|
||||
self.shutdown_event.clear()
|
||||
while not self.controller._min_scan_buffer_reached and not self.shutdown_event.wait(0.001):
|
||||
...
|
||||
self.controller.start_scan()
|
||||
self.shutdown_event.wait(0.1)
|
||||
status = DeviceStatus(self)
|
||||
self.start_readout(status)
|
||||
return status
|
||||
|
||||
def stop(self, *, success=False):
|
||||
self.shutdown_event.set()
|
||||
return super().stop(success=success)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
rtcontroller = RtFlomniController(
|
||||
socket_cls=SocketIO, socket_host="mpc2844.psi.ch", socket_port=2222, device_manager=None
|
||||
|
||||
@@ -152,8 +152,8 @@ class RtLamniController(Controller):
|
||||
# set these as closed loop target position
|
||||
self.socket_put(f"pa0,{x_curr:.4f}")
|
||||
self.socket_put(f"pa1,{y_curr:.4f}")
|
||||
self.dm.devices.rtx.obj.user_setpoint.set_with_feedback_disabled(x_curr)
|
||||
self.dm.devices.rty.obj.user_setpoint.set_with_feedback_disabled(y_curr)
|
||||
self.device_manager.devices.rtx.obj.user_setpoint.set_with_feedback_disabled(x_curr)
|
||||
self.device_manager.devices.rty.obj.user_setpoint.set_with_feedback_disabled(y_curr)
|
||||
self.socket_put("J5")
|
||||
logger.info("LamNI Feedback enabled (without reset).")
|
||||
self.set_device_read_write("lsamx", False)
|
||||
@@ -286,7 +286,7 @@ class RtLamniController(Controller):
|
||||
|
||||
# if not (mode==2 or mode==3):
|
||||
# error
|
||||
self.get_device_manager().connector.set(
|
||||
self.device_manager.connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=1, metadata=self.readout_metadata
|
||||
@@ -321,7 +321,7 @@ class RtLamniController(Controller):
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
self.get_device_manager().connector.set(
|
||||
self.device_manager.connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=0, metadata=self.readout_metadata
|
||||
@@ -333,7 +333,7 @@ class RtLamniController(Controller):
|
||||
)
|
||||
|
||||
def publish_device_data(self, signals, point_id):
|
||||
self.get_device_manager().connector.set_and_publish(
|
||||
self.device_manager.connector.set_and_publish(
|
||||
MessageEndpoints.device_read("rt_lamni"),
|
||||
messages.DeviceMessage(
|
||||
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
|
||||
@@ -368,10 +368,10 @@ class RtLamniController(Controller):
|
||||
) # 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)
|
||||
self.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()
|
||||
self.device_manager.devices.lsamx.obj.controller.lgalil_is_air_off_and_orchestra_enabled()
|
||||
)
|
||||
|
||||
if galil_controller_rt_status == 0:
|
||||
@@ -384,16 +384,16 @@ class RtLamniController(Controller):
|
||||
|
||||
time.sleep(0.03)
|
||||
|
||||
lsamx_user_params = self.get_device_manager().devices.lsamx.user_parameter
|
||||
lsamx_user_params = self.device_manager.devices.lsamx.user_parameter
|
||||
if lsamx_user_params is None or lsamx_user_params.get("center") is None:
|
||||
raise RuntimeError("lsamx center is not defined")
|
||||
lsamy_user_params = self.get_device_manager().devices.lsamy.user_parameter
|
||||
lsamy_user_params = self.device_manager.devices.lsamy.user_parameter
|
||||
if lsamy_user_params is None or lsamy_user_params.get("center") is None:
|
||||
raise RuntimeError("lsamy center is not defined")
|
||||
lsamx_center = lsamx_user_params.get("center")
|
||||
lsamy_center = lsamy_user_params.get("center")
|
||||
self.get_device_manager().devices.lsamx.obj.move(lsamx_center, wait=True)
|
||||
self.get_device_manager().devices.lsamy.obj.move(lsamy_center, wait=True)
|
||||
self.device_manager.devices.lsamx.obj.move(lsamx_center, wait=True)
|
||||
self.device_manager.devices.lsamy.obj.move(lsamy_center, wait=True)
|
||||
self.socket_put("J1")
|
||||
|
||||
_waitforfeedbackctr = 0
|
||||
@@ -588,10 +588,14 @@ class RtLamniMotor(Device, PositionerBase):
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
def wait_for_connection(self, timeout: int = 10, **kwargs) -> None:
|
||||
"""Wait for the device to be connected."""
|
||||
def wait_for_connection(self, timeout: float = 30.0) -> bool:
|
||||
self.controller.on(timeout=timeout)
|
||||
|
||||
def destroy(self):
|
||||
"""Make sure to turn off the controller socket on destroy."""
|
||||
self.controller.off(update_config=False)
|
||||
return super().destroy()
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@@ -270,7 +270,7 @@ class RtOMNYController(Controller):
|
||||
return self.mirror_parameters[channel]
|
||||
|
||||
def laser_tracker_check_and_wait_for_signalstrength(self):
|
||||
self.get_device_manager().connector.send_client_info(
|
||||
self.device_manager.connector.send_client_info(
|
||||
"Checking laser tracker...", scope="", show_asap=True
|
||||
)
|
||||
if not self.laser_tracker_check_enabled():
|
||||
@@ -282,12 +282,12 @@ class RtOMNYController(Controller):
|
||||
self.laser_tracker_wait_on_target()
|
||||
# when on target, check interferometer signal
|
||||
signal = self._omny_interferometer_get_signalsample("ssi_4", 0.1)
|
||||
rtx = self.get_device_manager().devices.rtx
|
||||
rtx = self.device_manager.devices.rtx
|
||||
min_signal = rtx.user_parameter.get("min_signal")
|
||||
low_signal = rtx.user_parameter.get("low_signal")
|
||||
wait_counter = 0
|
||||
while signal < min_signal and wait_counter < 10:
|
||||
self.get_device_manager().connector.send_client_info(
|
||||
self.device_manager.connector.send_client_info(
|
||||
f"The signal of the tracker {signal} is below the minimum required signal of {min_signal}. Waiting...",
|
||||
scope="laser_tracker_check_and_wait_for_signalstrength",
|
||||
show_asap=True,
|
||||
@@ -298,14 +298,14 @@ class RtOMNYController(Controller):
|
||||
signal = self._omny_interferometer_get_signalsample("ssi_4", 0.1)
|
||||
|
||||
if signal < low_signal:
|
||||
self.get_device_manager().connector.send_client_info(
|
||||
self.device_manager.connector.send_client_info(
|
||||
f"\x1b[91mThe signal of the tracker {signal} is below the low limit of {low_signal}. Auto readjustment...\x1b[0m",
|
||||
scope="laser_tracker_check_and_wait_for_signalstrength",
|
||||
show_asap=True,
|
||||
)
|
||||
|
||||
self.omny_interferometer_align_tracking()
|
||||
self.get_device_manager().connector.send_client_info(
|
||||
self.device_manager.connector.send_client_info(
|
||||
"Checking laser tracker completed.", scope="", show_asap=True
|
||||
)
|
||||
|
||||
@@ -364,7 +364,7 @@ class RtOMNYController(Controller):
|
||||
elif amplitude < 10:
|
||||
amplitude = 10
|
||||
|
||||
oshield = self.get_device_manager().devices.oshield
|
||||
oshield = self.device_manager.devices.oshield
|
||||
|
||||
oshield.obj.controller.move_open_loop_steps(
|
||||
channel, steps, amplitude=amplitude, frequency=500
|
||||
@@ -444,7 +444,7 @@ class RtOMNYController(Controller):
|
||||
|
||||
info_str = f"\rAuto aligning Channel {mirror_channel}, {opt_mirrorname}, Current signal: {current_sample:.0f}"
|
||||
message = info_str + " (q)uit \r"
|
||||
self.get_device_manager().connector.send_client_info(
|
||||
self.device_manager.connector.send_client_info(
|
||||
message, scope="_omny_interferometer_optimize", show_asap=True
|
||||
)
|
||||
|
||||
@@ -504,7 +504,7 @@ class RtOMNYController(Controller):
|
||||
return True
|
||||
|
||||
def feedback_enable_with_reset(self):
|
||||
self.get_device_manager().connector.send_client_info(
|
||||
self.device_manager.connector.send_client_info(
|
||||
f"Enabling the feedback...", scope="", show_asap=True
|
||||
)
|
||||
|
||||
@@ -525,16 +525,16 @@ class RtOMNYController(Controller):
|
||||
self.laser_tracker_on()
|
||||
time.sleep(0.01)
|
||||
|
||||
osamroy = self.get_device_manager().devices.osamroy
|
||||
osamroy = self.device_manager.devices.osamroy
|
||||
# the following read will also upate the angle in rt during readout
|
||||
readback = osamroy.obj.readback.get()
|
||||
if np.fabs(readback) > 0.1:
|
||||
self.get_device_manager().connector.send_client_info(
|
||||
self.device_manager.connector.send_client_info(
|
||||
f"Rotating to zero", scope="feedback enable", show_asap=True
|
||||
)
|
||||
osamroy.obj.move(0, wait=True)
|
||||
|
||||
osamx = self.get_device_manager().devices.osamx
|
||||
osamx = self.device_manager.devices.osamx
|
||||
|
||||
osamx_in = osamx.user_parameter.get("in")
|
||||
if not np.isclose(osamx.obj.readback.get(), osamx_in, atol=0.01):
|
||||
@@ -622,7 +622,7 @@ class RtOMNYController(Controller):
|
||||
# update variables and enable if not yet active
|
||||
if not self.laser_tracker_check_enabled():
|
||||
logger.info("Enabling the laser tracker. Please wait...")
|
||||
self.get_device_manager().connector.send_client_info(
|
||||
self.device_manager.connector.send_client_info(
|
||||
f"Enabling the laser tracker. Please wait...", scope="", show_asap=True
|
||||
)
|
||||
|
||||
@@ -639,12 +639,8 @@ class RtOMNYController(Controller):
|
||||
self.socket_put("T1")
|
||||
time.sleep(0.5)
|
||||
|
||||
self.get_device_manager().devices.otracky.obj.controller.socket_put_confirmed(
|
||||
"trackyct=0"
|
||||
)
|
||||
self.get_device_manager().devices.otracky.obj.controller.socket_put_confirmed(
|
||||
"trackzct=0"
|
||||
)
|
||||
self.device_manager.devices.otracky.obj.controller.socket_put_confirmed("trackyct=0")
|
||||
self.device_manager.devices.otracky.obj.controller.socket_put_confirmed("trackzct=0")
|
||||
|
||||
self.laser_tracker_wait_on_target()
|
||||
logger.info("Laser tracker running!")
|
||||
@@ -718,17 +714,17 @@ class RtOMNYController(Controller):
|
||||
self.laser_tracker_galil_status()
|
||||
|
||||
def laser_tracker_galil_enable(self):
|
||||
otracky_con = self.get_device_manager().devices.otracky.obj.controller
|
||||
otracky_con = self.device_manager.devices.otracky.obj.controller
|
||||
otracky_con.socket_put_confirmed("tracken=1")
|
||||
otracky_con.socket_put_confirmed("trackyct=0")
|
||||
otracky_con.socket_put_confirmed("trackzct=0")
|
||||
|
||||
def laser_tracker_galil_disable(self):
|
||||
otracky_con = self.get_device_manager().devices.otracky.obj.controller
|
||||
otracky_con = self.device_manager.devices.otracky.obj.controller
|
||||
otracky_con.socket_put_confirmed("tracken=0")
|
||||
|
||||
def laser_tracker_galil_status(self):
|
||||
otracky_con = self.get_device_manager().devices.otracky.obj.controller
|
||||
otracky_con = self.device_manager.devices.otracky.obj.controller
|
||||
if bool(float(otracky_con.socket_put_and_receive("MGtracken").strip())) == 0:
|
||||
print(self.red + "Tracking in the Galil Controller is disabled." + self.white)
|
||||
print("Use dev.rtx.controller.laser_tracker_galil_enable to enable.\n")
|
||||
@@ -914,12 +910,6 @@ class RtOMNYController(Controller):
|
||||
current_position_in_scan = int(float(return_table[2]))
|
||||
return (mode, number_of_positions_planned, current_position_in_scan)
|
||||
|
||||
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 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
|
||||
@@ -933,7 +923,7 @@ class RtOMNYController(Controller):
|
||||
|
||||
# if not (mode==2 or mode==3):
|
||||
# error
|
||||
self.get_device_manager().connector.set(
|
||||
self.device_manager.connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=1, metadata=self.readout_metadata
|
||||
@@ -968,7 +958,7 @@ class RtOMNYController(Controller):
|
||||
signals = self._get_signals_from_table(return_table)
|
||||
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
|
||||
|
||||
self.get_device_manager().connector.set(
|
||||
self.device_manager.connector.set(
|
||||
MessageEndpoints.device_status("rt_scan"),
|
||||
messages.DeviceStatusMessage(
|
||||
device="rt_scan", status=0, metadata=self.readout_metadata
|
||||
@@ -981,7 +971,7 @@ class RtOMNYController(Controller):
|
||||
f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}."
|
||||
)
|
||||
|
||||
self.get_device_manager().connector.send_client_info(
|
||||
self.device_manager.connector.send_client_info(
|
||||
"OMNY statistics: Average of all standard deviations [nm]: x"
|
||||
f" {self.average_stdeviations_x_st_fzp/read_counter*1000:.1f}, y"
|
||||
f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}.",
|
||||
@@ -990,7 +980,7 @@ class RtOMNYController(Controller):
|
||||
)
|
||||
|
||||
def publish_device_data(self, signals, point_id):
|
||||
self.get_device_manager().connector.set_and_publish(
|
||||
self.device_manager.connector.set_and_publish(
|
||||
MessageEndpoints.device_read("rt_omny"),
|
||||
messages.DeviceMessage(
|
||||
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
|
||||
@@ -1129,10 +1119,14 @@ class RtOMNYMotor(Device, PositionerBase):
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
def wait_for_connection(self, timeout: int = 10, **kwargs) -> None:
|
||||
"""Wait for the device to be connected."""
|
||||
def wait_for_connection(self, timeout: float = 30.0) -> bool:
|
||||
self.controller.on(timeout=timeout)
|
||||
|
||||
def destroy(self):
|
||||
"""Make sure to turn off the controller socket on destroy."""
|
||||
self.controller.off(update_config=False)
|
||||
return super().destroy()
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@@ -153,6 +153,14 @@ class SmaractMotor(Device, PositionerBase):
|
||||
self.low_limit_travel.put(limits[0])
|
||||
self.high_limit_travel.put(limits[1])
|
||||
|
||||
def wait_for_connection(self, timeout: float = 30.0) -> bool:
|
||||
self.controller.on(timeout=timeout)
|
||||
|
||||
def destroy(self):
|
||||
"""Make sure to turn off the controller socket on destroy."""
|
||||
self.controller.off(update_config=False)
|
||||
return super().destroy()
|
||||
|
||||
@property
|
||||
def limits(self):
|
||||
return (self.low_limit_travel.get(), self.high_limit_travel.get())
|
||||
|
||||
@@ -26,12 +26,12 @@ import numpy as np
|
||||
from bec_lib import bec_logger, messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_server.scan_server.errors import ScanAbortion
|
||||
from bec_server.scan_server.scans import AsyncFlyScanBase
|
||||
from bec_server.scan_server.scans import SyncFlyScanBase
|
||||
|
||||
logger = bec_logger.logger
|
||||
|
||||
|
||||
class FlomniFermatScan(AsyncFlyScanBase):
|
||||
class FlomniFermatScan(SyncFlyScanBase):
|
||||
scan_name = "flomni_fermat_scan"
|
||||
scan_type = "fly"
|
||||
required_kwargs = ["fovx", "fovy", "exp_time", "step", "angle"]
|
||||
@@ -97,7 +97,6 @@ class FlomniFermatScan(AsyncFlyScanBase):
|
||||
logger.warning("The zshift is smaller than -100 um. It will be limited to -100 um.")
|
||||
self.zshift = -100
|
||||
self.flomni_rotation_status = None
|
||||
self.flyer_num_pos = 0
|
||||
|
||||
def initialize(self):
|
||||
self.scan_motors = []
|
||||
@@ -108,6 +107,10 @@ class FlomniFermatScan(AsyncFlyScanBase):
|
||||
self.positions, corridor_size=self.optim_trajectory_corridor
|
||||
)
|
||||
|
||||
@property
|
||||
def monitor_sync(self):
|
||||
return "rt_flomni"
|
||||
|
||||
def reverse_trajectory(self):
|
||||
"""
|
||||
Reverse the trajectory. Every other scan should be reversed to
|
||||
@@ -132,13 +135,13 @@ class FlomniFermatScan(AsyncFlyScanBase):
|
||||
if flip_axes:
|
||||
self.positions = np.flipud(self.positions)
|
||||
|
||||
self.flyer_num_pos = len(self.positions)
|
||||
self.num_pos = len(self.positions)
|
||||
self._check_min_positions()
|
||||
|
||||
def _check_min_positions(self):
|
||||
if self.flyer_num_pos < 20:
|
||||
if self.num_pos < 20:
|
||||
raise ScanAbortion(
|
||||
f"The number of positions must exceed 20. Currently: {self.flyer_num_pos}."
|
||||
f"The number of positions must exceed 20. Currently: {self.num_pos}."
|
||||
)
|
||||
|
||||
def _prepare_setup(self):
|
||||
@@ -147,8 +150,6 @@ class FlomniFermatScan(AsyncFlyScanBase):
|
||||
|
||||
yield from self.stubs.send_rpc_and_wait("rty", "set", self.positions[0][1])
|
||||
|
||||
yield from self.stubs.scan_report_instruction({"device_progress": ["rt_flyer"]})
|
||||
|
||||
def _prepare_setup_part2(self):
|
||||
if self.flomni_rotation_status:
|
||||
self.flomni_rotation_status.wait()
|
||||
@@ -168,16 +169,12 @@ class FlomniFermatScan(AsyncFlyScanBase):
|
||||
tracker_signal_status = yield from self.stubs.send_rpc_and_wait(
|
||||
"rtx", "controller.laser_tracker_check_signalstrength"
|
||||
)
|
||||
# self.device_manager.connector.send_client_info(tracker_signal_status)
|
||||
#self.device_manager.connector.send_client_info(tracker_signal_status)
|
||||
if tracker_signal_status == "low":
|
||||
self.device_manager.connector.raise_alarm(
|
||||
severity=0,
|
||||
alarm_type="LaserTrackerSignalStrength",
|
||||
source={
|
||||
"device": "rtx",
|
||||
"reason": "low signal strength",
|
||||
"method": "_prepare_setup_part2",
|
||||
},
|
||||
source={"device": "rtx", "reason": "low signal strength", "method": "_prepare_setup_part2"},
|
||||
metadata={},
|
||||
msg="Signal strength of the laser tracker is low, sufficient to continue. Realignment recommended!",
|
||||
)
|
||||
@@ -279,13 +276,26 @@ class FlomniFermatScan(AsyncFlyScanBase):
|
||||
return np.array(positions)
|
||||
|
||||
def scan_core(self):
|
||||
status = yield from self.stubs.kickoff(device="rt_flyer", wait=False)
|
||||
while not status.done:
|
||||
yield from self.stubs.read(group="monitored", point_id=self.point_id)
|
||||
# use a device message to receive the scan number and
|
||||
# scan ID before sending the message to the device server
|
||||
yield from self.stubs.kickoff(device="rtx")
|
||||
while True:
|
||||
yield from self.stubs.read(group="monitored")
|
||||
status = self.connector.get(MessageEndpoints.device_status("rt_scan"))
|
||||
if status:
|
||||
status_id = status.content.get("status", 1)
|
||||
request_id = status.metadata.get("RID")
|
||||
if status_id == 0 and self.metadata.get("RID") == request_id:
|
||||
break
|
||||
if status_id == 2 and self.metadata.get("RID") == request_id:
|
||||
raise ScanAbortion(
|
||||
"An error occured during the flomni readout:"
|
||||
f" {status.metadata.get('error')}"
|
||||
)
|
||||
|
||||
time.sleep(1)
|
||||
self.point_id += 1
|
||||
logger.debug("reading monitors")
|
||||
self.num_pos = self.point_id
|
||||
# yield from self.device_rpc("rtx", "controller.kickoff")
|
||||
|
||||
def move_to_start(self):
|
||||
"""return to the start position"""
|
||||
|
||||
@@ -1,298 +1,230 @@
|
||||
# pylint: skip-file
|
||||
import os
|
||||
import threading
|
||||
from typing import Generator
|
||||
from unittest import mock
|
||||
|
||||
import ophyd
|
||||
import pytest
|
||||
from bec_lib import messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_lib.file_utils import get_full_path
|
||||
from bec_server.device_server.tests.utils import DMMock
|
||||
from ophyd_devices.tests.utils import MockPV
|
||||
from ophyd_devices.interfaces.base_classes.psi_device_base import DeviceStoppedError
|
||||
from ophyd_devices.tests.utils import patched_device
|
||||
|
||||
from csaxs_bec.devices.epics.falcon_csaxs import FalconcSAXS, FalconTimeoutError
|
||||
from csaxs_bec.devices.tests_utils.utils import patch_dual_pvs
|
||||
from csaxs_bec.devices.epics.falcon_csaxs import (
|
||||
ACQUIRESTATUS,
|
||||
FalconcSAXS,
|
||||
MappingSource,
|
||||
TriggerSource,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mock_det():
|
||||
name = "falcon"
|
||||
prefix = "X12SA-SITORO:"
|
||||
def mock_det() -> Generator[FalconcSAXS, None, None]:
|
||||
"""Fixture to mock the FalconcSAXS device."""
|
||||
name = "mcs_csaxs"
|
||||
prefix = "X12SA-MCS-CSAXS:"
|
||||
dm = DMMock()
|
||||
with mock.patch.object(dm, "connector"):
|
||||
with (
|
||||
mock.patch(
|
||||
"ophyd_devices.interfaces.base_classes.bec_device_base.FileWriter"
|
||||
) as filemixin,
|
||||
mock.patch(
|
||||
"ophyd_devices.interfaces.base_classes.psi_detector_base.PSIDetectorBase._update_service_config"
|
||||
) as mock_service_config,
|
||||
):
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
mock_cl.get_pv = MockPV
|
||||
mock_cl.thread_class = threading.Thread
|
||||
with mock.patch.object(FalconcSAXS, "_init"):
|
||||
det = FalconcSAXS(name=name, prefix=prefix, device_manager=dm)
|
||||
patch_dual_pvs(det)
|
||||
det.TIMEOUT_FOR_SIGNALS = 0.1
|
||||
yield det
|
||||
with patched_device(
|
||||
FalconcSAXS,
|
||||
name="falcon",
|
||||
prefix="X12SA-SITORO:",
|
||||
device_manager=dm,
|
||||
_mock_pv_initial_value=1,
|
||||
) as dev:
|
||||
try:
|
||||
for dotted_name, device in dev.walk_subdevices(include_lazy=True):
|
||||
device.stage_sigs = {} # Remove stage signals
|
||||
device.trigger_sigs = {} # Remove trigger signals
|
||||
if hasattr(device, "plugin_type"):
|
||||
device.plugin_type._read_pv.mock_data = device._plugin_type
|
||||
yield dev
|
||||
finally:
|
||||
dev.destroy()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"trigger_source, mapping_source, ignore_gate, pixels_per_buffer, detector_state,"
|
||||
" expected_exception",
|
||||
[(1, 1, 0, 20, 0, False), (1, 1, 0, 20, 1, True)],
|
||||
)
|
||||
# TODO rewrite this one, write test for init_detector, init_filewriter is tested
|
||||
def test_init_detector(
|
||||
mock_det,
|
||||
trigger_source,
|
||||
mapping_source,
|
||||
ignore_gate,
|
||||
pixels_per_buffer,
|
||||
detector_state,
|
||||
expected_exception,
|
||||
):
|
||||
"""Test the _init function:
|
||||
def test_falcon_init(mock_det: FalconcSAXS):
|
||||
"""Test the initialization of the FalconcSAXS device."""
|
||||
assert mock_det._readout_time == mock_det.MIN_READOUT
|
||||
assert mock_det._value_pixel_per_buffer == 20
|
||||
assert mock_det._queue_size == 2000
|
||||
assert mock_det._full_path == ""
|
||||
|
||||
This includes testing the functions:
|
||||
- _init_detector
|
||||
- _stop_det
|
||||
- _set_trigger
|
||||
--> Testing the filewriter is done in test_init_filewriter
|
||||
|
||||
Validation upon setting the correct PVs
|
||||
def test_falcon_on_connected(mock_det: FalconcSAXS):
|
||||
"""Test the on_connected method of the FalconcSAXS device."""
|
||||
falcon = mock_det
|
||||
|
||||
# Set known default values
|
||||
falcon.preset_mode.put(-1)
|
||||
falcon.input_logic_polarity.put(-1)
|
||||
falcon.auto_pixels_per_buffer.put(-1)
|
||||
falcon.hdf5.enable.put(-1)
|
||||
|
||||
with (
|
||||
mock.patch.object(falcon, "on_stop") as mock_on_stop,
|
||||
mock.patch.object(falcon, "set_trigger") as mock_set_trigger,
|
||||
):
|
||||
|
||||
falcon.on_connected()
|
||||
mock_on_stop.assert_called_once()
|
||||
mock_set_trigger.assert_called_once_with(
|
||||
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
|
||||
)
|
||||
|
||||
# Detector default PV values
|
||||
assert falcon.preset_mode.get() == "1" # Real Time
|
||||
assert falcon.input_logic_polarity.get() == 0
|
||||
assert falcon.auto_pixels_per_buffer.get() == 0
|
||||
assert falcon.pixels_per_buffer.get() == falcon._value_pixel_per_buffer
|
||||
|
||||
# Backend default PV values
|
||||
assert falcon.hdf5.enable.get() == "1" # Enabled
|
||||
assert falcon.hdf5.xml_file_name.get() == "layout.xml"
|
||||
assert falcon.hdf5.lazy_open.get() == "1" # Enabled
|
||||
assert falcon.hdf5.temp_suffix.get() == ""
|
||||
assert falcon.hdf5.queue_size.get() == falcon._queue_size
|
||||
assert falcon.nd_array_mode.get() == 1
|
||||
assert falcon.hdf5.file_template.get() == "%s%s"
|
||||
assert falcon.hdf5.file_write_mode.get() == 2
|
||||
|
||||
|
||||
def test_falcon_on_stage(mock_det: FalconcSAXS):
|
||||
"""
|
||||
mock_det.value_pixel_per_buffer = pixels_per_buffer
|
||||
mock_det.state._read_pv.mock_data = detector_state
|
||||
if expected_exception:
|
||||
with pytest.raises(FalconTimeoutError):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.initialize_detector()
|
||||
else:
|
||||
mock_det.custom_prepare.initialize_detector()
|
||||
assert mock_det.state.get() == detector_state
|
||||
assert mock_det.collect_mode.get() == mapping_source
|
||||
assert mock_det.pixel_advance_mode.get() == trigger_source
|
||||
assert mock_det.ignore_gate.get() == ignore_gate
|
||||
|
||||
assert mock_det.preset_mode.get() == 1
|
||||
assert mock_det.erase_all.get() == 1
|
||||
assert mock_det.input_logic_polarity.get() == 0
|
||||
assert mock_det.auto_pixels_per_buffer.get() == 0
|
||||
assert mock_det.pixels_per_buffer.get() == pixels_per_buffer
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"readout_time, expected_value", [(1e-3, 3e-3), (3e-3, 3e-3), (5e-3, 5e-3), (None, 3e-3)]
|
||||
)
|
||||
def test_update_readout_time(mock_det, readout_time, expected_value):
|
||||
if readout_time is None:
|
||||
mock_det.custom_prepare.update_readout_time()
|
||||
assert mock_det.readout_time == expected_value
|
||||
else:
|
||||
mock_det.scaninfo.readout_time = readout_time
|
||||
mock_det.custom_prepare.update_readout_time()
|
||||
assert mock_det.readout_time == expected_value
|
||||
|
||||
|
||||
def test_initialize_default_parameter(mock_det):
|
||||
with mock.patch.object(
|
||||
mock_det.custom_prepare, "update_readout_time"
|
||||
) as mock_update_readout_time:
|
||||
mock_det.custom_prepare.initialize_default_parameter()
|
||||
assert mock_det.value_pixel_per_buffer == 20
|
||||
mock_update_readout_time.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo",
|
||||
[
|
||||
(
|
||||
{
|
||||
"eacc": "e12345",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"exp_time": 0.1,
|
||||
"filepath": "test.h5",
|
||||
"scan_id": "123",
|
||||
"mokev": 12.4,
|
||||
}
|
||||
)
|
||||
],
|
||||
)
|
||||
def test_stage(mock_det, scaninfo):
|
||||
"""Test the stage function:
|
||||
|
||||
This includes testing _prep_det
|
||||
Test the on_stage method of the FalconcSAXS device.
|
||||
All relevant information is available in the scan_info attribute and used
|
||||
to bootstrap the detector for the upcoming acquisition. Two scenarios are tested:
|
||||
I. Normal case with exposure time larger than readout time
|
||||
II. Case where exposure time is smaller than readout time, which should raise an exception.
|
||||
"""
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "set_trigger") as mock_set_trigger,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "prepare_data_backend"
|
||||
) as mock_prep_data_backend,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "publish_file_location"
|
||||
) as mock_publish_file_location,
|
||||
mock.patch.object(mock_det.custom_prepare, "arm_acquisition") as mock_arm_acquisition,
|
||||
):
|
||||
mock_det.scaninfo.exp_time = scaninfo["exp_time"]
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.stage()
|
||||
mock_set_trigger.assert_called_once()
|
||||
assert mock_det.preset_real.get() == scaninfo["exp_time"]
|
||||
assert mock_det.pixels_per_run.get() == int(
|
||||
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
)
|
||||
mock_prep_data_backend.assert_called_once()
|
||||
mock_publish_file_location.assert_called_once_with(done=False, successful=False)
|
||||
mock_arm_acquisition.assert_called_once()
|
||||
falcon = mock_det
|
||||
num_points = 10
|
||||
exp_time = 0.2
|
||||
frames_per_trigger = 5
|
||||
falcon.scan_info.msg.num_points = num_points
|
||||
falcon.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger
|
||||
falcon.scan_info.msg.scan_parameters["exp_time"] = exp_time
|
||||
falcon.hdf5.array_counter.put(5) # Set to non-zero to check reset
|
||||
|
||||
# I. Normal case
|
||||
falcon.stage()
|
||||
|
||||
assert falcon.staged is ophyd.Staged.yes
|
||||
assert falcon._full_path == get_full_path(falcon.scan_info.msg, falcon.name)
|
||||
file_path = falcon.hdf5.file_path.get()
|
||||
file_name = falcon.hdf5.file_name.get()
|
||||
assert os.path.join(file_path, file_name) == falcon._full_path
|
||||
|
||||
assert falcon.preset_real_time.get() == exp_time
|
||||
assert falcon.pixels_per_run.get() == num_points * frames_per_trigger
|
||||
assert falcon.hdf5.num_capture.get() == num_points * frames_per_trigger
|
||||
assert falcon.hdf5.array_counter.get() == 0
|
||||
assert falcon.hdf5.capture.get() == 1
|
||||
assert falcon.start_all.get() == 1
|
||||
|
||||
# II. Unstage device first
|
||||
falcon.unstage()
|
||||
exp_time = 1e-3 # Smaller than readout time
|
||||
falcon.scan_info.msg.scan_parameters["exp_time"] = exp_time
|
||||
with pytest.raises(ValueError):
|
||||
falcon.stage()
|
||||
assert falcon.staged is not ophyd.Staged.no
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo",
|
||||
[
|
||||
(
|
||||
{
|
||||
"filepath": "/das/work/p18/p18533/data/S00000-S00999/S00001/data.h5",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"filepath": "/das/work/p18/p18533/data/S00000-S00999/S00001/data1234.h5",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_prepare_data_backend(mock_det, scaninfo):
|
||||
mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"]
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.scaninfo.scan_number = 1
|
||||
mock_det.custom_prepare.prepare_data_backend()
|
||||
file_path, file_name = os.path.split(scaninfo["filepath"])
|
||||
assert mock_det.hdf5.file_path.get() == file_path
|
||||
assert mock_det.hdf5.file_name.get() == file_name
|
||||
assert mock_det.hdf5.file_template.get() == "%s%s"
|
||||
assert mock_det.hdf5.num_capture.get() == int(
|
||||
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
def test_falcon_on_pre_scan(mock_det: FalconcSAXS):
|
||||
"""Test the on_pre_scan method of the FalconcSAXS device."""
|
||||
falcon = mock_det
|
||||
# I. Test normal case with success
|
||||
falcon.acquire_busy._read_pv.mock_data = ACQUIRESTATUS.DONE
|
||||
falcon.hdf5.capture._read_pv.mock_data = ACQUIRESTATUS.DONE
|
||||
falcon = mock_det
|
||||
st = falcon.on_pre_scan()
|
||||
assert st.done is False
|
||||
assert st.success is False
|
||||
falcon.acquire_busy._read_pv.mock_data = ACQUIRESTATUS.ACQUIRING
|
||||
assert st.done is False
|
||||
assert st.success is False
|
||||
falcon.hdf5.capture._read_pv.mock_data = ACQUIRESTATUS.ACQUIRING
|
||||
st.wait(3)
|
||||
assert st.done is True
|
||||
assert st.success is True
|
||||
|
||||
# II. Test abort case with stop called
|
||||
falcon.acquire_busy._read_pv.mock_data = ACQUIRESTATUS.DONE
|
||||
falcon.hdf5.capture._read_pv.mock_data = ACQUIRESTATUS.DONE
|
||||
st = falcon.on_pre_scan()
|
||||
assert st.done is False
|
||||
assert st.success is False
|
||||
falcon.stop()
|
||||
with pytest.raises(DeviceStoppedError):
|
||||
st.wait(3)
|
||||
assert st.done is True
|
||||
assert st.success is False
|
||||
|
||||
|
||||
def test_falcon_stop(mock_det: FalconcSAXS):
|
||||
"""Test the stop method of the FalconcSAXS device."""
|
||||
falcon = mock_det
|
||||
|
||||
falcon.stop_all.put(0)
|
||||
falcon.hdf5.capture.put(1)
|
||||
falcon.erase_all.put(0)
|
||||
falcon.stop()
|
||||
assert falcon.stop_all.get() == 1
|
||||
assert falcon.hdf5.capture.get() == 0
|
||||
assert falcon.erase_all.get() == 1
|
||||
|
||||
|
||||
def test_falcon_complete(mock_det: FalconcSAXS):
|
||||
"""Test the complete method of the FalconcSAXS device."""
|
||||
falcon = mock_det
|
||||
|
||||
num_points = 10
|
||||
frames_per_trigger = 5
|
||||
falcon.scan_info.msg.num_points = num_points
|
||||
falcon.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger
|
||||
|
||||
# I. Test normal case with success
|
||||
falcon.dxp.current_pixel._read_pv.mock_data = num_points * frames_per_trigger - 1
|
||||
falcon.hdf5.array_counter._read_pv.mock_data = num_points * frames_per_trigger - 1
|
||||
falcon._full_path = "/tmp/fake_path/test.h5"
|
||||
st = falcon.on_complete()
|
||||
assert st.done is False
|
||||
assert st.success is False
|
||||
falcon.dxp.current_pixel._read_pv.mock_data = num_points * frames_per_trigger
|
||||
assert st.done is False
|
||||
assert st.success is False
|
||||
falcon.hdf5.array_counter._read_pv.mock_data = num_points * frames_per_trigger
|
||||
st.wait(3)
|
||||
assert st.done is True
|
||||
assert st.success is True
|
||||
assert falcon.file_event.get() == messages.FileMessage(
|
||||
file_path="/tmp/fake_path/test.h5",
|
||||
done=True,
|
||||
successful=True,
|
||||
device_name=falcon.name,
|
||||
file_type="h5",
|
||||
hinted_h5_entries=None,
|
||||
metadata={},
|
||||
)
|
||||
assert mock_det.hdf5.file_write_mode.get() == 2
|
||||
assert mock_det.hdf5.array_counter.get() == 0
|
||||
assert mock_det.hdf5.capture.get() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo",
|
||||
[
|
||||
({"filepath": "test.h5", "successful": True, "done": False, "scan_id": "123"}),
|
||||
({"filepath": "test.h5", "successful": False, "done": True, "scan_id": "123"}),
|
||||
],
|
||||
)
|
||||
def test_publish_file_location(mock_det, scaninfo):
|
||||
mock_det.scaninfo.scan_id = scaninfo["scan_id"]
|
||||
mock_det.filepath.set(scaninfo["filepath"]).wait()
|
||||
mock_det.custom_prepare.publish_file_location(
|
||||
done=scaninfo["done"], successful=scaninfo["successful"]
|
||||
# II. Test case where acquisition fails due to interruption
|
||||
falcon.dxp.current_pixel._read_pv.mock_data = num_points * frames_per_trigger - 1
|
||||
st = falcon.on_complete()
|
||||
assert st.done is False
|
||||
assert st.success is False
|
||||
falcon.stop()
|
||||
with pytest.raises(DeviceStoppedError):
|
||||
st.wait(3)
|
||||
assert falcon.file_event.get() == messages.FileMessage(
|
||||
file_path="/tmp/fake_path/test.h5",
|
||||
done=True,
|
||||
successful=False,
|
||||
device_name=falcon.name,
|
||||
file_type="h5",
|
||||
hinted_h5_entries=None,
|
||||
metadata={},
|
||||
)
|
||||
if scaninfo["successful"] is None:
|
||||
msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"])
|
||||
else:
|
||||
msg = messages.FileMessage(
|
||||
file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"]
|
||||
)
|
||||
expected_calls = [
|
||||
mock.call(
|
||||
MessageEndpoints.public_file(scaninfo["scan_id"], mock_det.name),
|
||||
msg,
|
||||
pipe=mock_det.connector.pipeline.return_value,
|
||||
),
|
||||
mock.call(
|
||||
MessageEndpoints.file_event(mock_det.name),
|
||||
msg,
|
||||
pipe=mock_det.connector.pipeline.return_value,
|
||||
),
|
||||
]
|
||||
assert mock_det.connector.set_and_publish.call_args_list == expected_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize("detector_state, expected_exception", [(1, False), (0, True)])
|
||||
def test_arm_acquisition(mock_det, detector_state, expected_exception):
|
||||
with mock.patch.object(mock_det, "stop") as mock_stop:
|
||||
mock_det.state._read_pv.mock_data = detector_state
|
||||
if expected_exception:
|
||||
with pytest.raises(FalconTimeoutError):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.arm_acquisition()
|
||||
mock_stop.assert_called_once()
|
||||
else:
|
||||
mock_det.custom_prepare.arm_acquisition()
|
||||
assert mock_det.start_all.get() == 1
|
||||
|
||||
|
||||
def test_trigger(mock_det):
|
||||
with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger:
|
||||
mock_det.trigger()
|
||||
mock_on_trigger.assert_called_once()
|
||||
|
||||
|
||||
def test_complete(mock_det):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "publish_file_location"
|
||||
) as mock_publish_file_location,
|
||||
):
|
||||
mock_det.stopped = False
|
||||
mock_det.complete()
|
||||
assert mock_finished.call_count == 1
|
||||
call = mock.call(done=True, successful=True)
|
||||
assert mock_publish_file_location.call_args == call
|
||||
|
||||
|
||||
def test_stop(mock_det):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "stop_detector_backend"
|
||||
) as mock_stop_detector_backend,
|
||||
):
|
||||
mock_det.stop()
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_stop_detector_backend.assert_called_once()
|
||||
assert mock_det.stopped is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stopped, scaninfo",
|
||||
[
|
||||
(False, {"num_points": 500, "frames_per_trigger": 1}),
|
||||
(True, {"num_points": 500, "frames_per_trigger": 1}),
|
||||
],
|
||||
)
|
||||
def test_finished(mock_det, stopped, scaninfo):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "stop_detector_backend"
|
||||
) as mock_stop_file_writer,
|
||||
):
|
||||
mock_det.stopped = stopped
|
||||
mock_det.dxp.current_pixel._read_pv.mock_data = int(
|
||||
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
)
|
||||
mock_det.hdf5.array_counter._read_pv.mock_data = int(
|
||||
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
)
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.custom_prepare.finished()
|
||||
assert mock_det.stopped is stopped
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_stop_file_writer.assert_called_once()
|
||||
|
||||
@@ -2,9 +2,19 @@ import copy
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
from bec_server.device_server.tests.utils import DMMock
|
||||
from ophyd_devices.tests.utils import SocketMock
|
||||
|
||||
from csaxs_bec.devices.npoint.npoint import NPointAxis, NPointController
|
||||
from csaxs_bec.devices.omny.galil.fgalil_ophyd import FlomniGalilController, FlomniGalilMotor
|
||||
from csaxs_bec.devices.omny.galil.fupr_ophyd import FuprGalilController, FuprGalilMotor
|
||||
from csaxs_bec.devices.omny.galil.lgalil_ophyd import LamniGalilController, LamniGalilMotor
|
||||
from csaxs_bec.devices.omny.galil.ogalil_ophyd import OMNYGalilController, OMNYGalilMotor
|
||||
from csaxs_bec.devices.omny.galil.sgalil_ophyd import GalilController, SGalilMotor
|
||||
from csaxs_bec.devices.omny.rt.rt_flomni_ophyd import RtFlomniController, RtFlomniMotor
|
||||
from csaxs_bec.devices.omny.rt.rt_lamni_ophyd import RtLamniController, RtLamniMotor
|
||||
from csaxs_bec.devices.omny.rt.rt_omny_ophyd import RtOMNYController, RtOMNYMotor
|
||||
from csaxs_bec.devices.smaract.smaract_ophyd import SmaractController, SmaractMotor
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
@@ -161,3 +171,51 @@ def test_find_reference(leyex, axis_nr, socket_put_messages, socket_get_messages
|
||||
except Exception as e:
|
||||
print(e)
|
||||
assert leyex.controller.sock.buffer_put == socket_put_messages
|
||||
|
||||
|
||||
def test_wait_for_connection_called():
|
||||
"""Test that wait_for_connection is called on all motors that have a socket controller."""
|
||||
dm = DMMock()
|
||||
testable_connections = [
|
||||
(NPointAxis, NPointController),
|
||||
(FlomniGalilMotor, FlomniGalilController),
|
||||
(FuprGalilMotor, FuprGalilController),
|
||||
(LamniGalilMotor, LamniGalilController),
|
||||
(OMNYGalilMotor, OMNYGalilController),
|
||||
(SGalilMotor, GalilController),
|
||||
(RtFlomniMotor, RtFlomniController),
|
||||
(RtLamniMotor, RtLamniController),
|
||||
(RtOMNYMotor, RtOMNYController),
|
||||
(SmaractMotor, SmaractController),
|
||||
]
|
||||
for motor_cls, controller_cls in testable_connections:
|
||||
# Store values to restore later
|
||||
ctrl_axis_backup = controller_cls._axes_per_controller
|
||||
try:
|
||||
controller_cls._reset_controller()
|
||||
controller_cls._axes_per_controller = 3
|
||||
|
||||
motor = motor_cls(
|
||||
"C",
|
||||
name="test_motor",
|
||||
host="mpc2680.psi.ch",
|
||||
port=8081,
|
||||
socket_cls=SocketMock,
|
||||
device_manager=dm,
|
||||
)
|
||||
with mock.patch.object(motor.controller, "on") as mock_on:
|
||||
|
||||
motor.wait_for_connection(timeout=5.0)
|
||||
assert mock_on.call_args_list[-1] == mock.call(timeout=5.0)
|
||||
|
||||
# Make sure destroy calls controller off
|
||||
|
||||
with mock.patch.object(motor.controller, "off") as mock_off:
|
||||
motor.destroy()
|
||||
assert mock_off.call_count == 1
|
||||
assert mock_off.call_args_list[0] == mock.call(update_config=False)
|
||||
assert motor._destroyed is True
|
||||
|
||||
finally:
|
||||
controller_cls._reset_controller()
|
||||
controller_cls._axes_per_controller = ctrl_axis_backup
|
||||
|
||||
@@ -1,449 +0,0 @@
|
||||
# pylint: skip-file
|
||||
import os
|
||||
import threading
|
||||
from unittest import mock
|
||||
|
||||
import ophyd
|
||||
import pytest
|
||||
from bec_lib import messages
|
||||
from bec_lib.endpoints import MessageEndpoints
|
||||
from bec_server.device_server.tests.utils import DMMock
|
||||
from ophyd_devices.tests.utils import MockPV
|
||||
|
||||
from csaxs_bec.devices.epics.pilatus_csaxs import PilatuscSAXS
|
||||
from csaxs_bec.devices.tests_utils.utils import patch_dual_pvs
|
||||
|
||||
|
||||
@pytest.fixture(scope="function")
|
||||
def mock_det():
|
||||
name = "pilatus"
|
||||
prefix = "X12SA-ES-PILATUS300K:"
|
||||
dm = DMMock()
|
||||
with mock.patch.object(dm, "connector"):
|
||||
with (
|
||||
mock.patch("ophyd_devices.interfaces.base_classes.bec_device_base.FileWriter"),
|
||||
mock.patch(
|
||||
"ophyd_devices.interfaces.base_classes.psi_detector_base.PSIDetectorBase._update_service_config"
|
||||
),
|
||||
):
|
||||
with mock.patch.object(ophyd, "cl") as mock_cl:
|
||||
mock_cl.get_pv = MockPV
|
||||
mock_cl.thread_class = threading.Thread
|
||||
with mock.patch.object(PilatuscSAXS, "_init"):
|
||||
det = PilatuscSAXS(name=name, prefix=prefix, device_manager=dm)
|
||||
patch_dual_pvs(det)
|
||||
yield det
|
||||
|
||||
|
||||
@pytest.mark.parametrize("trigger_source, detector_state", [(1, 0)])
|
||||
# TODO rewrite this one, write test for init_detector, init_filewriter is tested
|
||||
def test_init_detector(mock_det, trigger_source, detector_state):
|
||||
"""Test the _init function:
|
||||
|
||||
This includes testing the functions:
|
||||
- _init_detector
|
||||
- _stop_det
|
||||
- _set_trigger
|
||||
--> Testing the filewriter is done in test_init_filewriter
|
||||
|
||||
Validation upon setting the correct PVs
|
||||
|
||||
"""
|
||||
mock_det.custom_prepare.on_init() # call the method you want to test
|
||||
assert mock_det.cam.acquire.get() == detector_state
|
||||
assert mock_det.cam.trigger_mode.get() == trigger_source
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo, stopped, expected_exception",
|
||||
[
|
||||
(
|
||||
{
|
||||
"eacc": "e12345",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"filepath": "test.h5",
|
||||
"scan_id": "123",
|
||||
"mokev": 12.4,
|
||||
},
|
||||
False,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
"eacc": "e12345",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"filepath": "test.h5",
|
||||
"scan_id": "123",
|
||||
"mokev": 12.4,
|
||||
},
|
||||
True,
|
||||
False,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_stage(mock_det, scaninfo, stopped, expected_exception):
|
||||
path = "tmp"
|
||||
mock_det.filepath_raw = path
|
||||
with mock.patch.object(
|
||||
mock_det.custom_prepare, "publish_file_location"
|
||||
) as mock_publish_file_location:
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"]
|
||||
mock_det.device_manager.add_device("mokev", value=12.4)
|
||||
mock_det.stopped = stopped
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "prepare_data_backend") as mock_data_backend,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "update_readout_time"
|
||||
) as mock_update_readout_time,
|
||||
):
|
||||
mock_det.filepath.set(scaninfo["filepath"]).wait()
|
||||
if expected_exception:
|
||||
with pytest.raises(Exception):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.stage()
|
||||
else:
|
||||
mock_det.stage()
|
||||
mock_data_backend.assert_called_once()
|
||||
mock_update_readout_time.assert_called()
|
||||
# Check _prep_det
|
||||
assert mock_det.cam.num_images.get() == int(
|
||||
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
|
||||
)
|
||||
assert mock_det.cam.num_frames.get() == 1
|
||||
|
||||
mock_publish_file_location.assert_called_once_with(
|
||||
done=False, successful=False, metadata={"input_path": path}
|
||||
)
|
||||
|
||||
|
||||
def test_pre_scan(mock_det):
|
||||
mock_det.custom_prepare.on_pre_scan()
|
||||
assert mock_det.cam.acquire.get() == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"readout_time, expected_value", [(1e-3, 3e-3), (3e-3, 3e-3), (5e-3, 5e-3), (None, 3e-3)]
|
||||
)
|
||||
def test_update_readout_time(mock_det, readout_time, expected_value):
|
||||
if readout_time is None:
|
||||
mock_det.custom_prepare.update_readout_time()
|
||||
assert mock_det.readout_time == expected_value
|
||||
else:
|
||||
mock_det.scaninfo.readout_time = readout_time
|
||||
mock_det.custom_prepare.update_readout_time()
|
||||
assert mock_det.readout_time == expected_value
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo",
|
||||
[
|
||||
(
|
||||
{
|
||||
"filepath": "test.h5",
|
||||
"filepath_raw": "test5_raw.h5",
|
||||
"successful": True,
|
||||
"done": False,
|
||||
"scan_id": "123",
|
||||
}
|
||||
),
|
||||
(
|
||||
{
|
||||
"filepath": "test.h5",
|
||||
"filepath_raw": "test5_raw.h5",
|
||||
"successful": False,
|
||||
"done": True,
|
||||
"scan_id": "123",
|
||||
}
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_publish_file_location(mock_det, scaninfo):
|
||||
mock_det.scaninfo.scan_id = scaninfo["scan_id"]
|
||||
mock_det.filepath.set(scaninfo["filepath"]).wait()
|
||||
mock_det.filepath_raw = scaninfo["filepath_raw"]
|
||||
mock_det.custom_prepare.publish_file_location(
|
||||
done=scaninfo["done"],
|
||||
successful=scaninfo["successful"],
|
||||
metadata={"input_path": scaninfo["filepath_raw"]},
|
||||
)
|
||||
if scaninfo["successful"] is None:
|
||||
msg = messages.FileMessage(
|
||||
file_path=scaninfo["filepath"],
|
||||
done=scaninfo["done"],
|
||||
metadata={"input_path": scaninfo["filepath_raw"]},
|
||||
)
|
||||
else:
|
||||
msg = messages.FileMessage(
|
||||
file_path=scaninfo["filepath"],
|
||||
done=scaninfo["done"],
|
||||
metadata={"input_path": scaninfo["filepath_raw"]},
|
||||
successful=scaninfo["successful"],
|
||||
)
|
||||
expected_calls = [
|
||||
mock.call(
|
||||
MessageEndpoints.public_file(scaninfo["scan_id"], mock_det.name),
|
||||
msg,
|
||||
pipe=mock_det.connector.pipeline.return_value,
|
||||
),
|
||||
mock.call(
|
||||
MessageEndpoints.file_event(mock_det.name),
|
||||
msg,
|
||||
pipe=mock_det.connector.pipeline.return_value,
|
||||
),
|
||||
]
|
||||
assert mock_det.connector.set_and_publish.call_args_list == expected_calls
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"requests_state, expected_exception, url_delete, url_put",
|
||||
[
|
||||
(
|
||||
True,
|
||||
False,
|
||||
"http://x12sa-pd-2:8080/stream/pilatus_2",
|
||||
"http://xbl-daq-34:8091/pilatus_2/stop",
|
||||
),
|
||||
(
|
||||
False,
|
||||
False,
|
||||
"http://x12sa-pd-2:8080/stream/pilatus_2",
|
||||
"http://xbl-daq-34:8091/pilatus_2/stop",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_stop_detector_backend(mock_det, requests_state, expected_exception, url_delete, url_put):
|
||||
with (
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "send_requests_delete"
|
||||
) as mock_send_requests_delete,
|
||||
mock.patch.object(mock_det.custom_prepare, "send_requests_put") as mock_send_requests_put,
|
||||
):
|
||||
instance_delete = mock_send_requests_delete.return_value
|
||||
instance_delete.ok = requests_state
|
||||
instance_put = mock_send_requests_put.return_value
|
||||
instance_put.ok = requests_state
|
||||
if expected_exception:
|
||||
mock_det.custom_prepare.stop_detector_backend()
|
||||
mock_send_requests_delete.assert_called_once_with(url=url_delete)
|
||||
mock_send_requests_put.assert_called_once_with(url=url_put)
|
||||
instance_delete.raise_for_status.called_once()
|
||||
instance_put.raise_for_status.called_once()
|
||||
else:
|
||||
mock_det.custom_prepare.stop_detector_backend()
|
||||
mock_send_requests_delete.assert_called_once_with(url=url_delete)
|
||||
mock_send_requests_put.assert_called_once_with(url=url_put)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"scaninfo, data_msgs, urls, requests_state, expected_exception",
|
||||
[
|
||||
(
|
||||
{
|
||||
"filepath_raw": "pilatus_2.h5",
|
||||
"eacc": "e12345",
|
||||
"scan_number": 1000,
|
||||
"scan_directory": "S00000_00999",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"headers": {"Content-Type": "application/json", "Accept": "application/json"},
|
||||
},
|
||||
[
|
||||
{
|
||||
"source": [
|
||||
{
|
||||
"searchPath": "/",
|
||||
"searchPattern": "glob:*.cbf",
|
||||
"destinationPath": (
|
||||
"/sls/X12SA/data/e12345/Data10/pilatus_2/S00000_00999"
|
||||
),
|
||||
}
|
||||
]
|
||||
},
|
||||
[
|
||||
"zmqWriter",
|
||||
"e12345",
|
||||
{
|
||||
"addr": "tcp://x12sa-pd-2:8888",
|
||||
"dst": ["file"],
|
||||
"numFrm": 500,
|
||||
"timeout": 2000,
|
||||
"ifType": "PULL",
|
||||
"user": "e12345",
|
||||
},
|
||||
],
|
||||
["zmqWriter", "e12345", {"frmCnt": 500, "timeout": 2000}],
|
||||
],
|
||||
[
|
||||
"http://x12sa-pd-2:8080/stream/pilatus_2",
|
||||
"http://xbl-daq-34:8091/pilatus_2/run",
|
||||
"http://xbl-daq-34:8091/pilatus_2/wait",
|
||||
],
|
||||
True,
|
||||
False,
|
||||
),
|
||||
(
|
||||
{
|
||||
"filepath_raw": "pilatus_2.h5",
|
||||
"eacc": "e12345",
|
||||
"scan_number": 1000,
|
||||
"scan_directory": "S00000_00999",
|
||||
"num_points": 500,
|
||||
"frames_per_trigger": 1,
|
||||
"headers": {"Content-Type": "application/json", "Accept": "application/json"},
|
||||
},
|
||||
[
|
||||
{
|
||||
"source": [
|
||||
{
|
||||
"searchPath": "/",
|
||||
"searchPattern": "glob:*.cbf",
|
||||
"destinationPath": (
|
||||
"/sls/X12SA/data/e12345/Data10/pilatus_2/S00000_00999"
|
||||
),
|
||||
}
|
||||
]
|
||||
},
|
||||
[
|
||||
"zmqWriter",
|
||||
"e12345",
|
||||
{
|
||||
"addr": "tcp://x12sa-pd-2:8888",
|
||||
"dst": ["file"],
|
||||
"numFrm": 500,
|
||||
"timeout": 2000,
|
||||
"ifType": "PULL",
|
||||
"user": "e12345",
|
||||
},
|
||||
],
|
||||
["zmqWriter", "e12345", {"frmCnt": 500, "timeout": 2000}],
|
||||
],
|
||||
[
|
||||
"http://x12sa-pd-2:8080/stream/pilatus_2",
|
||||
"http://xbl-daq-34:8091/pilatus_2/run",
|
||||
"http://xbl-daq-34:8091/pilatus_2/wait",
|
||||
],
|
||||
False, # return of res.ok is False!
|
||||
True,
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_prep_file_writer(mock_det, scaninfo, data_msgs, urls, requests_state, expected_exception):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "close_file_writer") as mock_close_file_writer,
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_file_writer") as mock_stop_file_writer,
|
||||
mock.patch.object(mock_det, "filewriter") as mock_filewriter,
|
||||
mock.patch.object(mock_det.custom_prepare, "create_directory") as mock_create_directory,
|
||||
mock.patch.object(mock_det.custom_prepare, "send_requests_put") as mock_send_requests_put,
|
||||
):
|
||||
mock_det.scaninfo.scan_number = scaninfo["scan_number"]
|
||||
mock_det.scaninfo.num_points = scaninfo["num_points"]
|
||||
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
|
||||
mock_det.scaninfo.username = scaninfo["eacc"]
|
||||
mock_filewriter.compile_full_filename.return_value = scaninfo["filepath_raw"]
|
||||
mock_filewriter.get_scan_directory.return_value = scaninfo["scan_directory"]
|
||||
instance = mock_send_requests_put.return_value
|
||||
instance.ok = requests_state
|
||||
instance.raise_for_status.side_effect = Exception
|
||||
|
||||
if expected_exception:
|
||||
with pytest.raises(Exception):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.prepare_data_backend()
|
||||
mock_close_file_writer.assert_called_once()
|
||||
mock_stop_file_writer.assert_called_once()
|
||||
instance.raise_for_status.assert_called_once()
|
||||
else:
|
||||
mock_det.custom_prepare.prepare_data_backend()
|
||||
|
||||
mock_close_file_writer.assert_called_once()
|
||||
mock_stop_file_writer.assert_called_once()
|
||||
|
||||
# Assert values set on detector
|
||||
assert mock_det.cam.file_path.get() == "/dev/shm/zmq/"
|
||||
assert (
|
||||
mock_det.cam.file_name.get()
|
||||
== f"{scaninfo['eacc']}_2_{scaninfo['scan_number']:05d}"
|
||||
)
|
||||
assert mock_det.cam.auto_increment.get() == 1
|
||||
assert mock_det.cam.file_number.get() == 0
|
||||
assert mock_det.cam.file_format.get() == 0
|
||||
assert mock_det.cam.file_template.get() == "%s%s_%5.5d.cbf"
|
||||
# Remove last / from destinationPath
|
||||
mock_create_directory.assert_called_once_with(
|
||||
os.path.join(data_msgs[0]["source"][0]["destinationPath"])
|
||||
)
|
||||
assert mock_send_requests_put.call_count == 3
|
||||
|
||||
calls = [
|
||||
mock.call(url=url, data=data_msg, headers=scaninfo["headers"])
|
||||
for url, data_msg in zip(urls, data_msgs)
|
||||
]
|
||||
for call, mock_call in zip(calls, mock_send_requests_put.call_args_list):
|
||||
assert call == mock_call
|
||||
|
||||
|
||||
def test_complete(mock_det):
|
||||
path = "tmp"
|
||||
mock_det.filepath_raw = path
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished,
|
||||
mock.patch.object(
|
||||
mock_det.custom_prepare, "publish_file_location"
|
||||
) as mock_publish_file_location,
|
||||
):
|
||||
mock_det.complete()
|
||||
assert mock_finished.call_count == 1
|
||||
call = mock.call(done=True, successful=True, metadata={"input_path": path})
|
||||
assert mock_publish_file_location.call_args == call
|
||||
|
||||
|
||||
def test_stop(mock_det):
|
||||
with (
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_file_writer") as mock_stop_file_writer,
|
||||
mock.patch.object(mock_det.custom_prepare, "close_file_writer") as mock_close_file_writer,
|
||||
):
|
||||
mock_det.stop()
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_stop_file_writer.assert_called_once()
|
||||
mock_close_file_writer.assert_called_once()
|
||||
assert mock_det.stopped is True
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"stopped, mcs_stage_state, expected_exception",
|
||||
[
|
||||
(False, ophyd.Staged.no, False),
|
||||
(True, ophyd.Staged.no, True),
|
||||
(False, ophyd.Staged.yes, True),
|
||||
],
|
||||
)
|
||||
def test_finished(mock_det, stopped, mcs_stage_state, expected_exception):
|
||||
with (
|
||||
mock.patch.object(mock_det, "device_manager") as mock_dm,
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_file_writer") as mock_stop_file_friter,
|
||||
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
|
||||
mock.patch.object(mock_det.custom_prepare, "close_file_writer") as mock_close_file_writer,
|
||||
):
|
||||
mock_dm.devices.mcs.obj._staged = mcs_stage_state
|
||||
mock_det.stopped = stopped
|
||||
if expected_exception:
|
||||
with pytest.raises(Exception):
|
||||
mock_det.timeout = 0.1
|
||||
mock_det.custom_prepare.finished()
|
||||
assert mock_det.stopped is stopped
|
||||
mock_stop_file_friter.assert_called()
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_close_file_writer.assert_called_once()
|
||||
else:
|
||||
mock_det.custom_prepare.finished()
|
||||
if stopped:
|
||||
assert mock_det.stopped is stopped
|
||||
|
||||
mock_stop_file_friter.assert_called()
|
||||
mock_stop_det.assert_called_once()
|
||||
mock_close_file_writer.assert_called_once()
|
||||
@@ -17,24 +17,23 @@ def rt_flomni():
|
||||
socket_port=8081,
|
||||
device_manager=mock.MagicMock(),
|
||||
)
|
||||
with mock.patch.object(rt_flomni, "get_device_manager"):
|
||||
with mock.patch.object(rt_flomni, "sock"):
|
||||
rtx = mock.MagicMock(spec=RtFlomniMotor)
|
||||
rtx.name = "rtx"
|
||||
rtx.axis_Id = "A"
|
||||
rtx.axis_Id_numeric = 0
|
||||
rty = mock.MagicMock(spec=RtFlomniMotor)
|
||||
rty.name = "rty"
|
||||
rty.axis_Id = "B"
|
||||
rty.axis_Id_numeric = 1
|
||||
rtz = mock.MagicMock(spec=RtFlomniMotor)
|
||||
rtz.name = "rtz"
|
||||
rtz.axis_Id = "C"
|
||||
rtz.axis_Id_numeric = 2
|
||||
rt_flomni.set_axis(axis=rtx, axis_nr=0)
|
||||
rt_flomni.set_axis(axis=rty, axis_nr=1)
|
||||
rt_flomni.set_axis(axis=rtz, axis_nr=2)
|
||||
yield rt_flomni
|
||||
with mock.patch.object(rt_flomni, "sock"):
|
||||
rtx = mock.MagicMock(spec=RtFlomniMotor)
|
||||
rtx.name = "rtx"
|
||||
rtx.axis_Id = "A"
|
||||
rtx.axis_Id_numeric = 0
|
||||
rty = mock.MagicMock(spec=RtFlomniMotor)
|
||||
rty.name = "rty"
|
||||
rty.axis_Id = "B"
|
||||
rty.axis_Id_numeric = 1
|
||||
rtz = mock.MagicMock(spec=RtFlomniMotor)
|
||||
rtz.name = "rtz"
|
||||
rtz.axis_Id = "C"
|
||||
rtz.axis_Id_numeric = 2
|
||||
rt_flomni.set_axis(axis=rtx, axis_nr=0)
|
||||
rt_flomni.set_axis(axis=rty, axis_nr=1)
|
||||
rt_flomni.set_axis(axis=rtz, axis_nr=2)
|
||||
yield rt_flomni
|
||||
RtFlomniController._reset_controller()
|
||||
|
||||
|
||||
@@ -56,7 +55,7 @@ def test_rt_flomni_feedback_is_running(rt_flomni, return_value, is_running):
|
||||
|
||||
def test_feedback_enable_with_reset(rt_flomni):
|
||||
|
||||
device_manager = rt_flomni.get_device_manager()
|
||||
device_manager = rt_flomni.device_manager
|
||||
device_manager.devices.fsamx.user_parameter.get.return_value = 0.05
|
||||
device_manager.devices.fsamx.obj.readback.get.return_value = 0.05
|
||||
|
||||
@@ -72,7 +71,7 @@ def test_feedback_enable_with_reset(rt_flomni):
|
||||
|
||||
|
||||
def test_move_samx_to_scan_region(rt_flomni):
|
||||
device_manager = rt_flomni.get_device_manager()
|
||||
device_manager = rt_flomni.device_manager
|
||||
device_manager.devices.rtx.user_parameter.get.return_value = 1
|
||||
rt_flomni.move_samx_to_scan_region(20, 2)
|
||||
assert mock.call(b"v0\n") not in rt_flomni.sock.put.mock_calls
|
||||
|
||||
Reference in New Issue
Block a user