Source code for csaxs_bec.devices.epics.delay_generator_csaxs.ddg_2

"""
DDG2 delay generator

This module implements the DDG2 delay generator logic for the CSAXS beamline.
Please check also the code for DDG1, aswell as the attached PDF trigger_scheme_ddg1_ddg2.pdf

The DDG2 is responsible for creating a burst of triggers for all relevant detectors.
It will receive a be triggered from the DDG1 through the EXT/EN channel.

A brief summary of the DDG2 logic:
DELAY PAIRS:
- EXT/EN is connected to the DDG1 delay pair ab.
- DelayPair ab is connected to a multiplexer, multiplexing the trigger to the detectors.

DELAY CHANNELS:
- a = t0
- b = a + (exp_time - READOUT_TIMES)

Burst mode is enabled:
- Burst count is set to the number of frames per trigger.
- Burst delay is set to 0.
- Burst period is set to the exposure time.
"""

import time

from bec_lib.logger import bec_logger
from bec_server.scan_server.scans.scan_base import ScanInfo as ScanServerScanInfo
from ophyd_devices import DeviceStatus, StatusBase
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase

from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import (
    BURSTCONFIG,
    CHANNELREFERENCE,
    OUTPUTPOLARITY,
    STATUSBITS,
    TRIGGERSOURCE,
    AllChannelNames,
    ChannelConfig,
    DelayGeneratorCSAXS,
    LiteralChannels,
)
from csaxs_bec.devices.utils.utils import fetch_scan_info

logger = bec_logger.logger

########################
## DEFAULT SETTINGS ####
########################

# NOTE Default channel configuration for the DDG2 delay generator channels
_DEFAULT_CHANNEL_CONFIG: ChannelConfig = {
    "amplitude": 4.5,
    "offset": 0.0,
    "polarity": OUTPUTPOLARITY.POSITIVE,
    "mode": "ttl",
}

# NOTE Default IO configuration for all channels in DDG2
# Each channel uses the same default configuration as defined above
# If needed, individual channel configurations should be modified here.
DEFAULT_IO_CONFIG: dict[AllChannelNames, ChannelConfig] = {
    "t0": _DEFAULT_CHANNEL_CONFIG,
    "ab": _DEFAULT_CHANNEL_CONFIG,
    "cd": _DEFAULT_CHANNEL_CONFIG,
    "ef": _DEFAULT_CHANNEL_CONFIG,
    "gh": _DEFAULT_CHANNEL_CONFIG,
}

DEFAULT_TRIGGER_SOURCE: TRIGGERSOURCE = TRIGGERSOURCE.EXT_RISING_EDGE

# NOTE Default readout times for the detectors connected to DDG2
# These values are used to calculate the difference between the burst_period and the pulse width of
# individual channel pairs. They also mark a lower limit for the exposure time. Needs to be
# adjusted if the exposure time should possibly go below 0.2 ms.
DEFAULT_READOUT_TIMES = {"ab": 2e-4, "cd": 2e-4, "ef": 2e-4, "gh": 2e-4}  # 0.2 ms 5kHz

# NOTE Default refernce settings for each channel in DDG2
DEFAULT_REFERENCES: list[tuple[LiteralChannels, CHANNELREFERENCE]] = [
    ("A", CHANNELREFERENCE.T0),
    ("B", CHANNELREFERENCE.A),
    ("C", CHANNELREFERENCE.T0),
    ("D", CHANNELREFERENCE.C),
    ("E", CHANNELREFERENCE.T0),
    ("F", CHANNELREFERENCE.E),
    ("G", CHANNELREFERENCE.T0),
    ("H", CHANNELREFERENCE.G),
]

###############################
## DDG2 IMPLEMENTATION ########
###############################


[docs] class DDG2(PSIDeviceBase, DelayGeneratorCSAXS): """ Implementation of the DelayGenerator DDG2 for the cSAXS beamline. This delay generator is reponsible to create triggers for the detectors. It is configured in burst mode. Please check the module docstring, the module README and the attached PDF 'trigger_scheme_ddg1_ddg2.pdf' for more information about the expected cabling and trigger logic. The IOC prefix is 'X12SA-CPCL-DDG2:'. Args: name (str): Name of the device. prefix (str, optional): EPICS prefix for the device. Defaults to ''. scan_info (ScanInfo | None, optional): Scan info object. Defaults to None. device_manager (DeviceManagerBase | None, optional): Device manager. Defaults to None. Implementation of DelayGeneratorCSAXS for the CSAXS master trigger delay generator at X12SA-CPCL-DDG2. This device is responsible for creating triggers in burst mode and is connected to a multiplexer that distributes the trigger to the detectors. The DDG2 is triggered by the DDG1 through the EXT/EN channel. """
[docs] def on_init(self) -> None: """Initialize the device""" self.scan_parameters: ScanServerScanInfo | None = None
# pylint: disable=attribute-defined-outside-init
[docs] def on_connected(self) -> None: """ This method is called after the device is initialized and all signals are connected. This happens when a device configuration is loaded in BEC. It sets the default values for this device - intended to overwrite everything to a usable default state. For this purpose, we use the DEFAULT SETTINGS defined at the top of this module. The following procedure is followed: - Stop the DDG to ensure it is not running. - Then, we set the DEFAULT_IO_CONFIG for each channel, the trigger source to DEFAULT_TRIGGER_SOURCE, and the channel references to DEFAULT_REFERENCES. """ self.stop_ddg() # NOTE Please adjust the default settings under 'DEFAULT SETTINGS' at the top of this module if needed. # This makes sure that we have a well defined default state for the DDG2 device. for channel, config in DEFAULT_IO_CONFIG.items(): self.set_io_values(channel, **config) self.set_trigger(DEFAULT_TRIGGER_SOURCE) self.set_references_for_channels(DEFAULT_REFERENCES) # Set burst config self.burst_config.put(BURSTCONFIG.FIRST_CYCLE.value) # TODO As the IOC may be out of sync with the HW, we make sure that we set the default parameters # in the IOC to the expected values. In the past, we've experienced that IOC and HW can go out # of sync. self.burst_delay.put(1) time.sleep(0.02) # Give HW time to process self.burst_delay.put(0) time.sleep(0.02) self.burst_count.put(2) time.sleep(0.02) self.burst_count.put(1) time.sleep(0.02) self.burst_mode.put(1) time.sleep(0.02) self.burst_mode.put(0) time.sleep(0.02)
[docs] def on_stage(self) -> DeviceStatus | StatusBase | None: """ This method is called when the device is staged before a scan. All information about the scan is available through self.scan_info.msg at this point. The DDG2 needs to be configured to create a sequence of TTL pulses in burst mode that are sent to the detectors. It therefore needs to know the exposure time and frames per trigger from the self.scan_info.msg.scan_parameters. 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) ######################################## ### Burst mode settings ################ ######################################## # NOTE Only adjust settings if needed. DDG2 should always be in burst mode when used at CSAXS. if self.burst_mode.get() == 0: self.burst_mode.put(1) # Ensure that there is no delay for the burst if self.burst_delay.get() != 0: self.burst_delay.put(0) exp_time = self.scan_parameters.exp_time frames_per_trigger = self.scan_parameters.frames_per_trigger # NOTE Check if the exposure time is longer than all readout times. # Raise a ValueError if requested exposure time is too short. if any(exp_time <= rt for rt in DEFAULT_READOUT_TIMES.values()): raise ValueError( f"Exposure time {exp_time} is too short for the readout times {DEFAULT_READOUT_TIMES}" ) ######################################### ### Setup timing for burst and delays ### ######################################### # Burst Period DDG2 settings. Only adjust them if needed. if self.burst_count.get() != frames_per_trigger: self.burst_count.put(frames_per_trigger) if self.burst_period.get() != exp_time: self.burst_period.put(exp_time) # Calculate the pulse width for the channel pair 'ab' burst_pulse_width = exp_time - DEFAULT_READOUT_TIMES["ab"] # Trigger detectors with delay 0, and pulse width = exp_time - readout_time self.set_delay_pairs(channel="ab", delay=0, width=burst_pulse_width) logger.info(f"DDG {self.name} on_stage completed in {time.time() - start_time:.3f}s.")
[docs] def on_pre_scan(self): """ Method that is called just before a scan starts. It was observed that a short delay of 50ms improves the overall stability in operation. This may be removed as other parts were adjusted, but for now we will keep it as the delay is short. """ # NOTE Short delay to allow for the HW to process the commands before the scan starts. # This may no longer be needed after other adjustments, and may be removed in the future. time.sleep(0.05)
[docs] def on_trigger(self) -> DeviceStatus | StatusBase | None: """ DDG2 does not implement any trigger specific logic as it is triggered by DDG1 through the EXT/EN channel. """ pass
[docs] def on_stop(self) -> None: """Stop the delay generator""" self.stop_ddg()
if __name__ == "__main__": ddg = DDG2(name="ddg2", prefix="X12SA-CPCL-DDG2:") ddg.wait_for_connection(all_signals=True, timeout=30) ddg.summary()