diff --git a/src/dodal/beamlines/i11.py b/src/dodal/beamlines/i11.py index c9446937ca..7428ecd4e2 100644 --- a/src/dodal/beamlines/i11.py +++ b/src/dodal/beamlines/i11.py @@ -13,6 +13,9 @@ AutotunedCyberstarBlower, CyberstarBlower, ) + +# from dodal.devices.common_dcm import BaseDCM, PitchAndRollCrystal, RollCrystal +from dodal.devices.i11.dcm import DCM from dodal.devices.i11.diff_stages import ( DiffractometerBase, DiffractometerStage, @@ -124,3 +127,11 @@ def slits_4() -> Slits: @device_factory() def slits_5() -> Slits: return Slits(prefix=f"{PREFIX.beamline_prefix}-AL-SLITS-05:") + + +@device_factory() +def dcm() -> DCM: + return DCM( + prefix=f"{PREFIX.beamline_prefix}-MO-DCM-01:", + xtal_prefix=f"{PREFIX.beamline_prefix}-DI-DCM-01:", + ) diff --git a/src/dodal/devices/i11/dcm.py b/src/dodal/devices/i11/dcm.py new file mode 100644 index 0000000000..95dfa8a267 --- /dev/null +++ b/src/dodal/devices/i11/dcm.py @@ -0,0 +1,66 @@ +import numpy as np +from ophyd_async.core import Array1D, soft_signal_r_and_setter +from ophyd_async.epics.core import epics_signal_r +from ophyd_async.epics.motor import Motor + +from dodal.common.crystal_metadata import ( + CrystalMetadata, + MaterialsEnum, + make_crystal_metadata_from_material, +) +from dodal.devices.common_dcm import ( + DoubleCrystalMonochromator, + PitchAndRollCrystal, + StationaryCrystal, +) + + +class DCM(DoubleCrystalMonochromator[PitchAndRollCrystal, StationaryCrystal]): + """ + A double crystal monochromator (DCM), used to select the energy of the beam. + + perp describes the gap between the 2 DCM crystals which has to change as you alter + the angle to select the requested energy. + + offset ensures that the beam exits the DCM at the same point, regardless of energy. + """ + + def __init__( + self, + prefix: str, + xtal_prefix: str, + crystal_metadata: CrystalMetadata | None = None, + name: str = "", + ) -> None: + cm = crystal_metadata or make_crystal_metadata_from_material( + MaterialsEnum.Si, (1, 1, 1) + ) + with self.add_children_as_readables(): + self.perp_in_mm = Motor(prefix + "PERP") + + # temperatures + self.xtal1_holder_temp = epics_signal_r(str, xtal_prefix + "PT100-1.SEVR") + self.xtal1_temp = epics_signal_r(str, xtal_prefix + "PT100-2.SEVR") + + self.xtal2_heater_temp = epics_signal_r(str, xtal_prefix + "PT100-3.SEVR") + self.xtal2_temp = epics_signal_r(str, xtal_prefix + "PT100-4.SEVR") + + self.xtal1_heater_temp = epics_signal_r( + float, xtal_prefix + "H1:TTEMP:CALC" + ) + self.xtal2_heater_temp = epics_signal_r( + float, xtal_prefix + "H2:TTEMP:CALC" + ) + + self.crystal_metadata_usage, _ = soft_signal_r_and_setter( + str, initial_value=cm.usage + ) + self.crystal_metadata_type, _ = soft_signal_r_and_setter( + str, initial_value=cm.type + ) + reflection_array = np.array(cm.reflection) + self.crystal_metadata_reflection, _ = soft_signal_r_and_setter( + Array1D[np.uint64], + initial_value=reflection_array, + ) + super().__init__(prefix, PitchAndRollCrystal, StationaryCrystal, name) diff --git a/tests/devices/i11/test_i11_dcm.py b/tests/devices/i11/test_i11_dcm.py new file mode 100644 index 0000000000..ea3af0a439 --- /dev/null +++ b/tests/devices/i11/test_i11_dcm.py @@ -0,0 +1,31 @@ +import bluesky.plans as bp +import pytest +from bluesky.run_engine import RunEngine +from ophyd_async.core import init_devices +from ophyd_async.testing import ( + assert_emitted, +) + +from dodal.devices.i11.dcm import DCM + + +@pytest.fixture +async def i11_dcm() -> DCM: + async with init_devices(mock=True): + i11_dcm = DCM(prefix="x-MO-DCM-01:", xtal_prefix="x-DI-DCM-01:") + return i11_dcm + + +def test_count_i11_dcm( + run_engine: RunEngine, + run_engine_documents: dict[str, list[dict]], + i11_dcm: DCM, +): + run_engine(bp.count([i11_dcm])) + assert_emitted( + run_engine_documents, + start=1, + descriptor=1, + event=1, + stop=1, + )