fix(cont-grid): fix cont grid timing issue #231

Merged
appel_c merged 17 commits from updates_from_the_beamline_part2 into main 2026-06-23 13:07:51 +02:00
10 changed files with 223 additions and 66 deletions
+2 -2
View File
@@ -13,8 +13,8 @@ eiger_9:
description: Eiger 9M detector
deviceClass: csaxs_bec.devices.jungfraujoch.eiger_9m.Eiger9M
deviceConfig:
detector_distance: 2200
beam_center: [870, 1203]
detector_distance: 2150
beam_center: [860, 1219]
onFailure: raise
enabled: True
readoutPriority: async
+49 -13
View File
@@ -15,7 +15,7 @@ eyex:
user_offset_dir: 0
deviceTags:
- cSAXS
- owis_samx
- owis_eyex
onFailure: buffer
enabled: true
readoutPriority: baseline
@@ -33,7 +33,7 @@ eyey:
user_offset_dir: 0
deviceTags:
- cSAXS
- owis_samx
- owis_eyey
onFailure: buffer
enabled: true
readoutPriority: baseline
@@ -42,11 +42,11 @@ samx:
description: Owis motor stage samx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES18
motor_resolution: 0.000125
base_velocity: 0.00625
velocity: 1
backlash_distance: 0.0125
prefix: X12SA-ES2-ES03
motor_resolution: 0.00125
base_velocity: 0.0625
velocity: 10
backlash_distance: 0.125
acceleration: 0.2
user_offset_dir: 0
deviceTags:
@@ -60,20 +60,56 @@ samy:
description: Owis motor stage samx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES19
motor_resolution: 0.000125
base_velocity: 0.00625
velocity: 1
backlash_distance: 0.0125
prefix: X12SA-ES2-ES04
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
- owis_samy
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
# samx:
# description: Owis motor stage samx
# deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
# deviceConfig:
# prefix: X12SA-ES2-ES18
# motor_resolution: 0.000125
# base_velocity: 0.00625
# velocity: 1
# backlash_distance: 0.0125
# acceleration: 0.2
# user_offset_dir: 0
# deviceTags:
# - cSAXS
# - owis_samx
# onFailure: buffer
# enabled: true
# readoutPriority: baseline
# softwareTrigger: false
# samy:
# description: Owis motor stage samx
# deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
# deviceConfig:
# prefix: X12SA-ES2-ES19
# motor_resolution: 0.000125
# base_velocity: 0.00625
# velocity: 1
# backlash_distance: 0.0125
# acceleration: 0.2
# user_offset_dir: 0
# deviceTags:
# - cSAXS
# - owis_samx
# onFailure: buffer
# enabled: true
# readoutPriority: baseline
# softwareTrigger: false
# eye_cam:
# description: Camera Microscope
# deviceClass: csaxs_bec.devices.ids_cameras.ids_camera.IDSCamera
@@ -55,7 +55,7 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import
LiteralChannels,
StatusBitsCompareStatus,
)
from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import ACQUIRING
from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import ACQUIRING, suppress_mca_callbacks
from csaxs_bec.devices.utils.utils import fetch_scan_info
if TYPE_CHECKING: # pragma: no cover
@@ -135,7 +135,12 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
device_manager (DeviceManagerBase | None, optional): Device manager. Defaults to None.
"""
USER_ACCESS = ["keep_shutter_open_during_scan", "set_trigger", "get_shutter_to_open_delay"]
USER_ACCESS = [
"keep_shutter_open_during_scan",
"set_trigger",
"get_shutter_to_open_delay",
"prepare_mcs_on_trigger",
]
# TODO Consider using the 'fsh' device instead.
fast_shutter_readback = Cpt(
@@ -273,6 +278,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
- We set the delay pairs ef to be triggered after the shutter closes with a width of 1us to trigger the MCS card.
- Finally, we add a short sleep to ensure that the IOC and DDG HW process the values properly.
"""
logger.info(f"DDG {self.name} on_stage called.")
self.scan_parameters = fetch_scan_info(self.scan_info)
start_time = time.time()
@@ -359,7 +365,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
time.sleep(0.2)
logger.info(f"DDG {self.name} on_stage completed in {time.time() - start_time:.3f}s.")
def _prepare_mcs_on_trigger(self, mcs: MCSCardCSAXS) -> None:
def prepare_mcs_on_trigger(self) -> CompareStatus:
"""
This method is used by the DDG1 on_trigger method to prepare the MCS card for the next trigger.
@@ -368,6 +374,12 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
It relies on the MCS card implementation and needs to be adapted if the MCS card logic changes.
"""
mcs_name = "mcs"
if self.device_manager is None or not hasattr(self.device_manager, "devices"):
raise ValueError("Device manager is not properly initialized.")
mcs = self.device_manager.devices.get(mcs_name, None)
if mcs is None:
raise ValueError(f"MCS card {mcs_name} not found in device manager.")
# NOTE First we wait that the MCS card is not acquiring. We add here a timeout of 5s to avoid
# a deadlock in case the MCS card is stuck for some reason. This should not happen normally.
@@ -378,10 +390,12 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
# NOTE Clear the '_omit_mca_callbacks' flag. This makes sure that data received from the mca1...mca3
# counters are forwarded to BEC. Once the flag is set, we create a TransitionStatus DONE->ACQUIRING
# and start the acquisition through erase_start.put(1). Finally, we wait for the card to go to ACQUIRING state.
mcs._omit_mca_callbacks.clear() # pylint: disable=protected-access
status_acquiring = CompareStatus(mcs.acquiring, ACQUIRING.ACQUIRING)
with suppress_mca_callbacks(mcs):
mcs.erase_start.put(1)
mcs._omit_mca_callbacks.clear() # pylint: disable=protected-access
self.cancel_on_stop(status_acquiring)
mcs.erase_start.put(1)
return status_acquiring
@@ -564,7 +578,7 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
# NOTE If the MCS card is present in the current session of BEC,
# we prepare the card for the next trigger. The procedure is implemented
# in the '_prepare_mcs_on_trigger' method. We will also use the mcs card
# in the 'prepare_mcs_on_trigger' method. We will also use the mcs card
# to indicate when the burst cycle is done. If no mcs card is available
# the fallback is to use the polling of the DDG
mcs = self.device_manager.devices.get("mcs", None)
@@ -583,18 +597,36 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
else:
start_time = time.time()
logger.debug(f"Preparing mcs card ")
status_mcs = self._prepare_mcs_on_trigger(mcs)
status_mcs = self.prepare_mcs_on_trigger()
# NOTE Timeout of 3s should be plenty, any longer wait should checked. If this happens to crash
# an acquisition regularly with a WaitTimeoutError, the timeout can be increased but it should
# be investigated why the EPICS interface is slow to respond.
try:
# current_time = time.time()
# while time.time() - current_time < 3 and mcs.acquiring.get() != ACQUIRING.ACQUIRING:
# time.sleep(0.1)
# logger.warning(
# f"MCS card is not in acquiring state, current state: {mcs.acquiring.get()}"
# )
# if mcs.acquiring.get() != ACQUIRING.ACQUIRING:
# logger.error(
# f"MCS card is not finishing after 3s, current state: {mcs.acquiring.get()}, waiting another 3s for its return to ACQUIRING state. If this happens regularly, please check the EPICS interface and the MCS card logic."
# )
if not status_mcs.done:
mcs.acquiring.get(use_monitor=False)
status_mcs.wait(timeout=3)
except Exception as exc:
logger.warning(f"MCS did not go to Acquiring within 3s. Retrying erase_start {exc}")
mcs.erase_start.put(1)
status_mcs.wait(timeout=3)
status = TransitionStatus(mcs.acquiring, [ACQUIRING.ACQUIRING, ACQUIRING.DONE])
logger.debug(f"Finished preparing mcs card {time.time()-start_time}")
if (
mcs.acquiring.get(use_monitor=False) != ACQUIRING.ACQUIRING
): # Get the current state without monitoring to avoid any side effects
logger.warning(
f"MCS did not go to Acquiring within 3s. Retrying erase_start {exc}"
)
raise exc
# mcs.erase_start.put(1)
# status_mcs.wait(timeout=3)
status = CompareStatus(mcs.acquiring, ACQUIRING.DONE)
logger.info(f"Finished preparing mcs card {time.time()-start_time}")
# Send trigger
self.trigger_shot.put(1, use_complete=True)
@@ -173,6 +173,7 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS):
This logic is robust for step scans as well as fly scans, as the DDG2 is triggered by the DDG1
through the EXT/EN channel.
"""
logger.info(f"DDG {self.name} on_stage called.")
start_time = time.time()
self.scan_parameters = fetch_scan_info(self.scan_info)
########################################
@@ -508,6 +508,7 @@ class DelayGeneratorCSAXS(Device):
EpicsSignal,
"TriggerDelayBO",
name="trigger_shot",
put_complete=True,
kind=Kind.omitted,
doc="Software trigger, needs to be in correct mode to work",
)
+31 -1
View File
@@ -164,6 +164,8 @@ class MCSCard(Device):
- EPICS SIS3801 and SIS3820 Drivers: https://millenia.cars.aps.anl.gov/software/epics/mcaStruck.html
"""
WRITE_TIMEOUT = 4.0 # seconds
snl_connected = Cpt(
EpicsSignalRO,
"SNL_Connected",
@@ -175,18 +177,21 @@ class MCSCard(Device):
EpicsSignal,
"EraseAll",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Erases all mca or waveform records, setting elapsed times and counts in all channels to 0. Please note that this operation sends the mca or waveform records to process after erasing, potentially also 0s.",
)
erase_start = Cpt(
EpicsSignal,
"EraseStart",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Erases all mca or waveform records and starts acquisition.",
)
start_all = Cpt(
EpicsSignal,
"StartAll",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Starts or resumes acquisition without erasing first.",
)
acquiring = Cpt(
@@ -196,11 +201,18 @@ class MCSCard(Device):
auto_monitor=True,
doc="Acquiring (=1) when acquisition is in progress and Done (=0) when acquisition is complete.",
)
stop_all = Cpt(EpicsSignal, "StopAll", kind=Kind.omitted, doc="Stops acquisition.")
stop_all = Cpt(
EpicsSignal,
"StopAll",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Stops acquisition.",
)
preset_real = Cpt(
EpicsSignal,
"PresetReal",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Preset real time. If non-zero then acquisition will stop when this time is reached.",
)
elapsed_real = Cpt(
@@ -213,72 +225,84 @@ class MCSCard(Device):
EpicsSignal,
"DoReadAll.VAL",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Forces a read of all mca or waveform records from the hardware. This record can be set to periodically process to update the records during acquisition. Note that even if this record has SCAN=Passive the mca or waveform records will always process once when acquisition completes.",
)
read_mode = Cpt(
EpicsSignal,
"ReadAll.SCAN",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Readout mode for transferring data from FIFO buffer to mca EPICS scalars.",
)
num_use_all = Cpt(
EpicsSignal,
"NuseAll",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="The number of channels to use for the mca or waveform records. Acquisition will automatically stop when the number of channel advances reaches this value.",
)
dwell = Cpt(
EpicsSignal,
"Dwell",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="The dwell time per channel when using internal channel advance mode.",
)
channel_advance = Cpt(
EpicsSignal,
"ChannelAdvance",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="The channel advance mode. Choices are 'Internal' (count for a preset time per channel) or 'External' (advance on external hardware channel advance signal).",
)
count_on_start = Cpt(
EpicsSignal,
"CountOnStart",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Flag controlling whether the module begins counting immediately when acquisition starts. This record only applies in External channel advance mode. If No (=0) then counting does not start in channel 0 until receipt of the first external channel advance pulse. If Yes (=1) then counting in channel 0 starts immediately when acquisition starts, without waiting for the first external channel advance pulse.",
)
software_channel_advance = Cpt(
EpicsSignal,
"SoftwareChannelAdvance",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Processing this record causes a channel advance to occur immediately, without waiting for the current dwell time to be reached or the next external channel advance pulse to arrive.",
)
channel1_source = Cpt(
EpicsSignal,
"Channel1Source",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Controls the source of pulses into the first counter. The choices are 'Int. clock' which selects the internal clock, and 'External' which selects the external pulse input to counter 1.",
)
prescale = Cpt(
EpicsSignal,
"Prescale",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="The prescale factor for external channel advance pulses. If the prescale factor is N then N external channel advance pulses must be received before a channel advance will occur.",
)
enable_client_wait = Cpt(
EpicsSignal,
"EnableClientWait",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Flag to force acquisition to wait until a client clears the ClientWait busy record before proceeding to the next acquisition. This can be useful with the scan record.",
)
client_wait = Cpt(
EpicsSignal,
"ClientWait",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Flag that will be set to 1 when acquisition completes, and which a client must set back to 0 to allow acquisition to proceed. This only has an effect if EnableClientWait is 1.",
)
acquire_mode = Cpt(
EpicsSignal,
"AcquireMode",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="The current acquisition mode (MCS=0 or Scaler=1). This record is used to turn off the scaler record Autocount in MCS mode.",
)
# NOTE: Setting mux_output programmatically results in occasional errors on the IOC; it is recommended to avoid using it.
@@ -286,36 +310,42 @@ class MCSCard(Device):
EpicsSignal,
"MUXOutput",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Value of 0-32 used to select which input signal is routed to output signal 7 on the SIS3820 in output mode 3. NOTE: This settings seems to occasionally result in errors on the IOC; it is recommended to avoid using it.",
)
user_led = Cpt(
EpicsSignal,
"UserLED",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="Toggles the user LED and also output signal 8 on the SIS3820.",
)
input_mode = Cpt(
EpicsSignal,
"InputMode",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="The input mode. Supported input modes vary for SIS3801 and SIS3820.",
)
input_polarity = Cpt(
EpicsSignal,
"InputPolarity",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="The polarity of the input control signals on the SIS3820. Choices are Normal and Inverted.",
)
output_mode = Cpt(
EpicsSignal,
"OutputMode",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="The output mode. Supported output modes vary for SIS3801 and SIS3820.",
)
output_polarity = Cpt(
EpicsSignal,
"OutputPolarity",
kind=Kind.omitted,
write_timeout=WRITE_TIMEOUT,
doc="The polarity of the output control signals on the SIS3820. Choices are Normal and Inverted.",
)
model = Cpt(
@@ -68,13 +68,14 @@ def suppress_mca_callbacks(mcs_card: MCSCard, restore_after_timeout: None | floa
clear the suppression after the specified time. If None, the original state
is not restored.
"""
mcs_card._omit_mca_callbacks.set() # pylint: disable=protected-access
try:
yield
finally:
if restore_after_timeout is not None:
time.sleep(restore_after_timeout)
mcs_card._omit_mca_callbacks.clear() # pylint: disable=protected-access
with mcs_card._rlock:
mcs_card._omit_mca_callbacks.set() # pylint: disable=protected-access
try:
yield
finally:
if restore_after_timeout is not None:
time.sleep(restore_after_timeout)
mcs_card._omit_mca_callbacks.clear() # pylint: disable=protected-access
if TYPE_CHECKING: # pragma: no cover
@@ -97,7 +98,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
prefix (str, optional): Prefix for the EPICS PVs. Defaults to "".
"""
USER_ACCESS = ["mcs_recovery"]
USER_ACCESS = ["mcs_recovery", "get_transition_status", "get_compare_status"]
# NOTE The number of MCA channels is fixed to 32 for the CSAXS MCS card.
# On the IOC, we receive a 'warning' or 'error' once we set this channel for the
@@ -297,7 +298,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
# Once we have received all channels, push data to BEC and reset for next accumulation
if len(self._current_data) == self.NUM_MCA_CHANNELS:
logger.debug(
logger.info(
f"Current data index {self._current_data_index} complete, pushing to BEC."
)
self.mca.put(self._current_data, acquisition_group=self._acquisition_group)
@@ -342,6 +343,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
- Clear any events and buffers related to async data emission. This includes '_omit_mca_callbacks',
'_start_monitor_async_data_emission', '_scan_done_callbacks', and '_current_data'.
"""
logger.info(f"MCS Card {self.name} on_stage called.")
start_time = time.time()
self.scan_parameters = fetch_scan_info(self.scan_info)
@@ -379,11 +381,11 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
self._num_lines = self.scan_parameters.additional_scan_parameters.get("num_lines", 1)
self._current_line = 1
self._acquisition_group = "monitored" if triggers == 1 else "burst_group"
self.preset_real.set(0).wait(timeout=self._pv_timeout)
self.preset_real.set(0).wait() # TODO consider using put...
if self.scan_parameters.scan_type == "software_triggered":
self.num_use_all.set(triggers).wait(timeout=self._pv_timeout)
self.num_use_all.set(triggers).wait()
elif self.scan_parameters.scan_type == "hardware_triggered":
self.num_use_all.set(self._num_total_triggers).wait(timeout=self._pv_timeout)
self.num_use_all.set(self._num_total_triggers).wait()
# Clear any previous data, just to be sure
with self._rlock:
@@ -459,6 +461,9 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
and self.scan_parameters.num_points is not None
):
if self.scan_parameters.scan_type == "software_triggered":
logger.info(
f"Software triggered scan: {self._current_data_index}/{self.scan_parameters.num_points} points received."
)
if self._current_data_index == self.scan_parameters.num_points:
for callback in self._scan_done_callbacks:
callback(exception=None)
@@ -482,13 +487,12 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
self._start_monitor_async_data_emission.clear() # Stop monitoring
# NOTE Important check as set_finished or set_exception should not be called
# if the status is already done (e.g. cancelled externally)
with self._rlock:
if status.done:
return # Already done and cancelled externally.
if exception is not None:
status.set_exception(exception)
else:
status.set_finished()
if status.done:
return # Already done and cancelled externally.
if exception is not None:
status.set_exception(exception)
else:
status.set_finished()
def _status_failed_callback(self, status: StatusBase) -> None:
"""Callback for status failure, the monitoring thread should be stopped."""
@@ -543,7 +547,7 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
self.software_channel_advance.put(1)
# Prepare and register status callback for the async monitoring loop
status_async_data = StatusBase(obj=self)
status_async_data = StatusBase(obj=self, timeout=5)
self._scan_done_callbacks.append(partial(self._status_callback, status_async_data))
# Set the event to start monitoring async data emission
@@ -551,12 +555,14 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
self._start_monitor_async_data_emission.set()
# Add CompareStatus for Acquiring DONE
status = CompareStatus(self.acquiring, ACQUIRING.DONE)
status = CompareStatus(self.acquiring, ACQUIRING.DONE, timeout=5)
# status.wait(timeout=3) # timeout is passed to individual status objects, so don't wait here.
# Combine both statuses
ret_status = status & status_async_data
# NOTE: Handle external stop/cancel, and stop monitoring
ret_status.add_callback(self._status_failed_callback)
# ret_status.wait(timeout=3) # timeout is passed to individual status objects, so don't wait here.
self.cancel_on_stop(ret_status)
return ret_status
@@ -601,3 +607,32 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
# We restore the callback suppression after timeout to ensure proper operation afterwards.
with suppress_mca_callbacks(self, restore_after_timeout=sleep_time):
self.erase_all.put(1)
def get_transition_status(self, signal_name: str, transition: list) -> TransitionStatus:
"""
Get the transition status for a signal of the device.
Args:
signal_name: The name of the signal to check the transition status for.
transition (list): List of transitions to check.
"""
signal = getattr(self, signal_name, None)
if signal is None:
raise ValueError(f"Signal {signal_name} not found in device {self.name}.")
return TransitionStatus(signal, transition)
def get_compare_status(
self, signal_name: str, expected_value: any, operation_success: str = "=="
) -> CompareStatus:
"""
Get the compare status for a signal of the device.
Args:
signal_name: The name of the signal to check the compare status for.
expected_value: The expected value to compare against.
operation_success: The comparison operation to use. Defaults to "==".
"""
signal = getattr(self, signal_name, None)
if signal is None:
raise ValueError(f"Signal {signal_name} not found in device {self.name}.")
return CompareStatus(signal, expected_value, operation_success=operation_success)
+2 -1
View File
@@ -270,6 +270,7 @@ class Eiger(PSIDeviceBase):
"""
Hook called when staging the device. Information about the upcoming scan can be accessed from the scan_info object.
"""
logger.info(f"Device {self.name} on_stage called.")
start_time = time.time()
self.scan_parameters = fetch_scan_info(self.scan_info)
@@ -332,7 +333,7 @@ class Eiger(PSIDeviceBase):
logger.debug(f"Setting data_settings: {yaml.dump(data_settings.to_dict(), indent=4)}")
prep_time = time.time()
self.jfj_client.wait_for_idle(timeout=10) # Ensure we are in IDLE state
self.jfj_client.start(settings=data_settings) # Takes around ~0.6s
self.jfj_client.start(settings=data_settings) # Takes around ~0.6s -> 6s currently.
# Time the stage process
logger.info(
+30 -9
View File
@@ -17,6 +17,7 @@ Scan procedure:
from __future__ import annotations
import time
from copy import deepcopy
from typing import Annotated, TypedDict
import numpy as np
@@ -185,26 +186,28 @@ class ContGrid(ScanBase):
)
positions = position_generators.nd_grid_positions(
[
(self.fast_start, self.fast_end, frames_per_trigger),
(self.stepper_start, self.stepper_stop, self._cont_motor_params["num_lines"]),
(self.fast_start, self.fast_end, frames_per_trigger),
],
snaked=False,
)
# Count only the end point of each line as a valid position, as the fast axis is continuously moving and only triggered at
# the beginning of the line moving to the end point.
self.positions = positions[(frames_per_trigger - 1) :: frames_per_trigger, :]
positions = positions[:, ::-1]
# Get device specific parameters
self._fetch_device_params()
# Adjust relative positions if needed
if self.relative:
self.start_positions = self.components.get_start_positions(self.motors)
self.positions += self.start_positions
positions += self.start_positions
self.fast_start += self.start_positions[0]
self.fast_end += self.start_positions[0]
self.stepper_start += self.start_positions[1]
self.stepper_stop += self.start_positions[1]
self.positions = deepcopy(positions[(frames_per_trigger - 1) :: frames_per_trigger, :])
# Adjust premove
self.fast_start -= self._cont_motor_params["premove_distance"]
self.fast_end += self._cont_motor_params["premove_distance"]
@@ -265,10 +268,27 @@ class ContGrid(ScanBase):
"""
# Only use every second position, at each point will use
for line_index in range(self._cont_motor_params["num_lines"]):
self.actions.set(
self.motors, [self.fast_start, self.positions[line_index][1]], wait=True
line_start = time.time()
status_mcs = self.ddg1.prepare_mcs_on_trigger()
status_motor = self.actions.set(
self.motors, [self.fast_start, self.positions[line_index][1]]
)
status_motor.wait()
logger.info(
f"Overhead from motor motion for line {line_index}: {(time.time() - line_start):.02f}s"
)
status_mcs.wait()
mcs_aquiring_status = self.mcs.get_transition_status(
signal_name="acquiring", transition=[1, 0]
)
logger.info(
f"Overhead before calling trigger for line {line_index}: {(time.time() - line_start):.02f}s"
)
self.at_each_point(
motors=[self.fast_axis],
positions=np.array([self.fast_end]),
status_mcs=mcs_aquiring_status,
)
self.at_each_point(motors=[self.fast_axis], positions=np.array([self.fast_end]))
self._restore_motor_properties()
@scan_hook
@@ -276,6 +296,7 @@ class ContGrid(ScanBase):
self,
motors: list[str | DeviceBase],
positions: np.ndarray,
status_mcs,
last_positions: np.ndarray | None = None,
):
"""
@@ -287,7 +308,7 @@ class ContGrid(ScanBase):
self.fast_axis.acceleration.set(self._cont_motor_params["acc_time"]).wait(timeout=5)
move_status = self.actions.set(motors, positions, wait=False)
time.sleep(self._cont_motor_params["acc_time"])
trigger_status = self.ddg1.trigger()
self.ddg1.trigger_shot.put(1)
while not move_status.done:
self.actions.read_monitored_devices(wait=True)
try:
@@ -296,10 +317,10 @@ class ContGrid(ScanBase):
continue
try:
trigger_status.wait(timeout=2)
status_mcs.wait(timeout=3)
except TimeoutError as exc:
raise ScanAbortion(
f"Status for delay generator trigger {self.ddg1.name} did not resolve after 2 seconds. "
f"MCS card did not go back to DONE after receiving all triggers and an extra 3 seconds. "
) from exc
@scan_hook
@@ -260,7 +260,7 @@ def test_ddg1_prepare_mcs(mock_ddg1: DDG1, mock_mcs_csaxs: MCSCardCSAXS):
mcs.erase_start.put(0) # reset erase start
# Prepare MCS on trigger
st = ddg._prepare_mcs_on_trigger(mcs)
st = ddg.prepare_mcs_on_trigger()
assert st.done is False
assert st.success is False
assert mcs.erase_start.get() == 1 # erase started
@@ -356,16 +356,16 @@ def test_ddg1_on_trigger(mock_ddg1: DDG1):
#################################
# Scenario I - normal operation #
#################################
with mock.patch.object(ddg, "_prepare_mcs_on_trigger") as mock_prepare_mcs:
with mock.patch.object(ddg, "prepare_mcs_on_trigger") as mock_prepare_mcs:
# Set acquioring PV to acquiring
mcs = ddg.device_manager.devices.get("mcs", None)
assert mcs is not None
mcs.acquiring._read_pv.mock_data = 1 # Simulate acquiring started
mock_prepare_mcs.return_value = ophyd.StatusBase(done=True, success=True)
# MCS card is present and enabled, should call prepare_mcs_on_trigger
# and the status should resolve once acuiring goes from 1 to 0.
status = ddg.trigger()
assert status.done is False
mcs = ddg.device_manager.devices.get("mcs", None)
assert mcs is not None
mcs.acquiring._read_pv.mock_data = 1 # Simulate acquiring started
assert status.done is False
mcs.acquiring._read_pv.mock_data = 0 # Simulate acquiring stopped
status.wait(timeout=1) # Wait for the status to be done
assert status.done is True
@@ -407,7 +407,7 @@ def test_ddg1_on_trigger(mock_ddg1: DDG1):
ddg.state.event_status._read_pv.mock_data = STATUSBITS.ABORT_DELAY.value
ddg._start_polling()
assert ddg._poll_thread_run_event.is_set()
with mock.patch.object(ddg, "_prepare_mcs_on_trigger") as mock_prepare_mcs:
with mock.patch.object(ddg, "prepare_mcs_on_trigger") as mock_prepare_mcs:
status = ddg.trigger()
mock_prepare_mcs.assert_not_called() # MCS is disabled, should not be called
assert status.done is False