Source code for dodal.devices.undulator_dcm

import asyncio

import numpy as np
from bluesky.protocols import Movable
from numpy import argmin, ndarray
from ophyd_async.core import AsyncStatus, StandardReadable

from dodal.common.beamlines.beamline_parameters import get_beamline_parameters
from dodal.log import LOGGER

from .dcm import DCM
from .undulator import Undulator, UndulatorGapAccess
from .util.lookup_tables import energy_distance_table

ENERGY_TIMEOUT_S: float = 30.0
STATUS_TIMEOUT_S: float = 10.0

# Enable to allow testing when the beamline is down, do not change in production!
TEST_MODE = False


class AccessError(Exception):
    pass


def _get_closest_gap_for_energy(
    dcm_energy_ev: float, energy_to_distance_table: ndarray
) -> float:
    table = energy_to_distance_table.transpose()
    idx = argmin(np.abs(table[0] - dcm_energy_ev))
    return table[1][idx]


[docs] class UndulatorDCM(StandardReadable, Movable): """ Composite device to handle changing beamline energies, wraps the Undulator and the DCM. The DCM has a motor which controls the beam energy, when it moves, the Undulator gap may also have to change to enable emission at the new energy. The relationship between the two motor motor positions is provided via a lookup table. Calling unulator_dcm.set(energy) will move the DCM motor, perform a table lookup and move the Undulator gap motor if needed. So the set method can be thought of as a comprehensive way to set beam energy. """ def __init__( self, undulator: Undulator, dcm: DCM, id_gap_lookup_table_path: str, daq_configuration_path: str, prefix: str = "", name: str = "", ): super().__init__(name) # Attributes are set after super call so they are not renamed to # <name>-undulator, etc. self.undulator = undulator self.dcm = dcm # These attributes are just used by hyperion for lookup purposes self.id_gap_lookup_table_path = id_gap_lookup_table_path self.dcm_pitch_converter_lookup_table_path = ( daq_configuration_path + "/lookup/BeamLineEnergy_DCM_Pitch_converter.txt" ) self.dcm_roll_converter_lookup_table_path = ( daq_configuration_path + "/lookup/BeamLineEnergy_DCM_Roll_converter.txt" ) # I03 configures the DCM Perp as a side effect of applying this fixed value to the DCM Offset after an energy change # Nb this parameter is misleadingly named to confuse you self.dcm_fixed_offset_mm = get_beamline_parameters( daq_configuration_path + "/domain/beamlineParameters" )["DCM_Perp_Offset_FIXED"] def set(self, value: float) -> AsyncStatus: async def _set(): await asyncio.gather( self._set_dcm_energy(value), self._set_undulator_gap_if_required(value), ) return AsyncStatus(_set()) async def _set_dcm_energy(self, energy_kev: float) -> None: access_level = await self.undulator.gap_access.get_value() if access_level is UndulatorGapAccess.DISABLED and not TEST_MODE: raise AccessError("Undulator gap access is disabled. Contact Control Room") await self.dcm.energy_in_kev.set( energy_kev, timeout=ENERGY_TIMEOUT_S, ) async def _set_undulator_gap_if_required(self, energy_kev: float) -> None: LOGGER.info(f"Setting DCM energy to {energy_kev:.2f} kev") gap_to_match_dcm_energy = await self._gap_to_match_dcm_energy(energy_kev) # Check if undulator gap is close enough to the value from the DCM current_gap = await self.undulator.current_gap.get_value() tolerance = await self.undulator.gap_discrepancy_tolerance_mm.get_value() if abs(gap_to_match_dcm_energy - current_gap) > tolerance: LOGGER.info( f"Undulator gap mismatch. {abs(gap_to_match_dcm_energy-current_gap):.3f}mm is outside tolerance.\ Moving gap to nominal value, {gap_to_match_dcm_energy:.3f}mm" ) if not TEST_MODE: # Only move if the gap is sufficiently different to the value from the # DCM lookup table AND we're not in TEST_MODE await self.undulator.gap_motor.set( gap_to_match_dcm_energy, timeout=STATUS_TIMEOUT_S, ) else: LOGGER.debug("In test mode, not moving ID gap") else: LOGGER.debug( "Gap is already in the correct place for the new energy value " f"{energy_kev}, no need to ask it to move" ) async def _gap_to_match_dcm_energy(self, energy_kev: float) -> float: # Get 2d np.array converting energies to undulator gap distance, from lookup table energy_to_distance_table = await energy_distance_table( self.id_gap_lookup_table_path ) # Use the lookup table to get the undulator gap associated with this dcm energy return _get_closest_gap_for_energy( energy_kev * 1000, energy_to_distance_table, )