Skip to content

Commit e31405d

Browse files
Make BaseDCM (#1111)
* Create BaseDCM * Modify other DCM's to inherit from BaseDCM
1 parent 57f1138 commit e31405d

File tree

17 files changed

+165
-84
lines changed

17 files changed

+165
-84
lines changed

src/dodal/beamlines/i03.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,15 @@
1818
from dodal.devices.backlight import Backlight
1919
from dodal.devices.baton import Baton
2020
from dodal.devices.cryostream import CryoStream
21-
from dodal.devices.dcm import DCM
2221
from dodal.devices.detector.detector_motion import DetectorMotion
2322
from dodal.devices.diamond_filter import DiamondFilter, I03Filters
2423
from dodal.devices.eiger import EigerDetector
2524
from dodal.devices.fast_grid_scan import PandAFastGridScan, ZebraFastGridScan
2625
from dodal.devices.flux import Flux
2726
from dodal.devices.focusing_mirror import FocusingMirrorWithStripes, MirrorVoltages
2827
from dodal.devices.i03.beamstop import Beamstop
28+
from dodal.devices.i03.dcm import DCM
29+
from dodal.devices.i03.undulator_dcm import UndulatorDCM
2930
from dodal.devices.motors import XYZPositioner
3031
from dodal.devices.oav.oav_detector import OAV
3132
from dodal.devices.oav.oav_parameters import OAVConfig
@@ -37,7 +38,6 @@
3738
from dodal.devices.synchrotron import Synchrotron
3839
from dodal.devices.thawer import Thawer
3940
from dodal.devices.undulator import Undulator
40-
from dodal.devices.undulator_dcm import UndulatorDCM
4141
from dodal.devices.webcam import Webcam
4242
from dodal.devices.xbpm_feedback import XBPMFeedback
4343
from dodal.devices.xspress3.xspress3 import Xspress3
@@ -113,8 +113,8 @@ def dcm() -> DCM:
113113
If this is called when already instantiated in i03, it will return the existing object.
114114
"""
115115
return DCM(
116-
f"{PREFIX.beamline_prefix}-MO-DCM-01:",
117-
"dcm",
116+
prefix=f"{PREFIX.beamline_prefix}-MO-DCM-01:",
117+
name="dcm",
118118
)
119119

120120

src/dodal/beamlines/i04.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,13 @@
1010
)
1111
from dodal.devices.attenuator.attenuator import BinaryFilterAttenuator
1212
from dodal.devices.backlight import Backlight
13-
from dodal.devices.dcm import DCM
1413
from dodal.devices.detector import DetectorParams
1514
from dodal.devices.detector.detector_motion import DetectorMotion
1615
from dodal.devices.diamond_filter import DiamondFilter, I04Filters
1716
from dodal.devices.eiger import EigerDetector
1817
from dodal.devices.fast_grid_scan import ZebraFastGridScan
1918
from dodal.devices.flux import Flux
19+
from dodal.devices.i03.dcm import DCM
2020
from dodal.devices.i04.transfocator import Transfocator
2121
from dodal.devices.ipin import IPin
2222
from dodal.devices.motors import XYZPositioner

src/dodal/beamlines/i18.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
LocalDirectoryServiceClient,
1313
StaticVisitPathProvider,
1414
)
15-
from dodal.devices.dcm import DCM
15+
from dodal.devices.common_dcm import BaseDCM, PitchAndRollCrystal, RollCrystal
1616
from dodal.devices.i18.diode import Diode
1717
from dodal.devices.i18.KBMirror import KBMirror
1818
from dodal.devices.i18.table import Table
@@ -54,12 +54,15 @@ def undulator() -> Undulator:
5454
return Undulator(f"{PREFIX.insertion_prefix}-MO-SERVC-01:")
5555

5656

57-
@device_factory()
58-
def dcm() -> DCM:
57+
# See https://github.com/DiamondLightSource/dodal/issues/1180
58+
@device_factory(skip=True)
59+
def dcm() -> BaseDCM[RollCrystal, PitchAndRollCrystal]:
5960
# once spacing is added Si111 d-spacing is 3.135 angsterm , and Si311 is 1.637
6061
# calculations are in gda/config/lookupTables/Si111/eV_Deg_converter.xml
61-
return DCM(
62+
return BaseDCM(
6263
prefix=f"{PREFIX.beamline_prefix}-MO-DCM-01:",
64+
xtal_1=RollCrystal,
65+
xtal_2=PitchAndRollCrystal,
6366
)
6467

6568

src/dodal/beamlines/i22.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from dodal.common.visit import RemoteDirectoryServiceClient, StaticVisitPathProvider
1919
from dodal.devices.bimorph_mirror import BimorphMirror
2020
from dodal.devices.focusing_mirror import FocusingMirror
21-
from dodal.devices.i22.dcm import DoubleCrystalMonochromator
21+
from dodal.devices.i22.dcm import DCM
2222
from dodal.devices.i22.fswitch import FSwitch
2323
from dodal.devices.i22.nxsas import NXSasMetadataHolder, NXSasOAV, NXSasPilatus
2424
from dodal.devices.linkam3 import Linkam3
@@ -141,8 +141,8 @@ def bimorph_vfm() -> BimorphMirror:
141141

142142

143143
@device_factory()
144-
def dcm() -> DoubleCrystalMonochromator:
145-
return DoubleCrystalMonochromator(
144+
def dcm() -> DCM:
145+
return DCM(
146146
prefix=f"{PREFIX.beamline_prefix}-MO-DCM-01:",
147147
temperature_prefix=f"{PREFIX.beamline_prefix}-DI-DCM-01:",
148148
crystal_1_metadata=make_crystal_metadata_from_material(

src/dodal/beamlines/p38.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717
from dodal.common.visit import LocalDirectoryServiceClient, StaticVisitPathProvider
1818
from dodal.devices.focusing_mirror import FocusingMirror
19-
from dodal.devices.i22.dcm import DoubleCrystalMonochromator
19+
from dodal.devices.i22.dcm import DCM
2020
from dodal.devices.i22.fswitch import FSwitch
2121
from dodal.devices.linkam3 import Linkam3
2222
from dodal.devices.pressure_jump_cell import PressureJumpCell
@@ -143,8 +143,8 @@ def hfm() -> FocusingMirror:
143143

144144

145145
@device_factory(mock=True)
146-
def dcm() -> DoubleCrystalMonochromator:
147-
return DoubleCrystalMonochromator(
146+
def dcm() -> DCM:
147+
return DCM(
148148
temperature_prefix=f"{PREFIX.beamline_prefix}-DI-DCM-01:",
149149
crystal_1_metadata=make_crystal_metadata_from_material(
150150
MaterialsEnum.Si, (1, 1, 1)

src/dodal/devices/common_dcm.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from typing import Generic, TypeVar
2+
3+
from ophyd_async.core import (
4+
StandardReadable,
5+
)
6+
from ophyd_async.epics.core import epics_signal_r
7+
from ophyd_async.epics.motor import Motor
8+
9+
10+
class StationaryCrystal(StandardReadable):
11+
def __init__(self, prefix):
12+
super().__init__(prefix)
13+
14+
15+
class RollCrystal(StationaryCrystal):
16+
def __init__(self, prefix):
17+
with self.add_children_as_readables():
18+
self.roll_in_mrad = Motor(prefix + "ROLL")
19+
super().__init__(prefix)
20+
21+
22+
class PitchAndRollCrystal(StationaryCrystal):
23+
def __init__(self, prefix):
24+
with self.add_children_as_readables():
25+
self.pitch_in_mrad = Motor(prefix + "PITCH")
26+
self.roll_in_mrad = Motor(prefix + "ROLL")
27+
super().__init__(prefix)
28+
29+
30+
Xtal_1 = TypeVar("Xtal_1", bound=StationaryCrystal)
31+
Xtal_2 = TypeVar("Xtal_2", bound=StationaryCrystal)
32+
33+
34+
class BaseDCM(StandardReadable, Generic[Xtal_1, Xtal_2]):
35+
"""
36+
Common device for the double crystal monochromator (DCM), used to select the energy of the beam.
37+
38+
Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
39+
each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
40+
This base device accounts for all combinations of this.
41+
42+
This device should act as a parent for beamline-specific DCM's, in which any other missing signals can be added.
43+
44+
Bluesky plans using DCM's should be typed to specify which types of crystals are required. For example, a plan
45+
which only requires one crystal which can roll should be typed 'def my_plan(dcm: BaseDCM[RollCrystal, StationaryCrystal])`
46+
"""
47+
48+
def __init__(
49+
self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
50+
) -> None:
51+
with self.add_children_as_readables():
52+
# Virtual motor PV's which set the physical motors so that the DCM produces requested
53+
# wavelength/energy
54+
self.energy_in_kev = Motor(prefix + "ENERGY")
55+
self.wavelength_in_a = Motor(prefix + "WAVELENGTH")
56+
57+
# Real motors
58+
self.bragg_in_degrees = Motor(prefix + "BRAGG")
59+
# Offset ensures that the beam exits the DCM at the same point, regardless of energy.
60+
self.offset_in_mm = Motor(prefix + "OFFSET")
61+
62+
self.crystal_metadata_d_spacing_a = epics_signal_r(
63+
float, prefix + "DSPACING:RBV"
64+
)
65+
66+
self._make_crystals(prefix, xtal_1, xtal_2)
67+
68+
super().__init__(name)
69+
70+
# Prefix convention is different depending on whether there are one or two controllable crystals
71+
def _make_crystals(self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2]):
72+
if StationaryCrystal not in [xtal_1, xtal_2]:
73+
self.xtal_1 = xtal_1(f"{prefix}XTAL1:")
74+
self.xtal_2 = xtal_2(f"{prefix}XTAL2:")
75+
else:
76+
self.xtal_1 = xtal_1(prefix)
77+
self.xtal_2 = xtal_2(prefix)

src/dodal/devices/dcm.py renamed to src/dodal/devices/i03/dcm.py

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import numpy as np
2-
from ophyd_async.core import Array1D, StandardReadable, soft_signal_r_and_setter
2+
from ophyd_async.core import Array1D, soft_signal_r_and_setter
33
from ophyd_async.epics.core import epics_signal_r
44
from ophyd_async.epics.motor import Motor
55

@@ -8,9 +8,14 @@
88
MaterialsEnum,
99
make_crystal_metadata_from_material,
1010
)
11+
from dodal.devices.common_dcm import (
12+
BaseDCM,
13+
PitchAndRollCrystal,
14+
StationaryCrystal,
15+
)
1116

1217

13-
class DCM(StandardReadable):
18+
class DCM(BaseDCM[PitchAndRollCrystal, StationaryCrystal]):
1419
"""
1520
A double crystal monochromator (DCM), used to select the energy of the beam.
1621
@@ -30,13 +35,7 @@ def __init__(
3035
MaterialsEnum.Si, (1, 1, 1)
3136
)
3237
with self.add_children_as_readables():
33-
self.bragg_in_degrees = Motor(prefix + "BRAGG")
34-
self.roll_in_mrad = Motor(prefix + "ROLL")
35-
self.offset_in_mm = Motor(prefix + "OFFSET")
3638
self.perp_in_mm = Motor(prefix + "PERP")
37-
self.energy_in_kev = Motor(prefix + "ENERGY")
38-
self.pitch_in_mrad = Motor(prefix + "PITCH")
39-
self.wavelength = Motor(prefix + "WAVELENGTH")
4039

4140
# temperatures
4241
self.xtal1_temp = epics_signal_r(float, prefix + "TEMP1")
@@ -58,7 +57,4 @@ def __init__(
5857
Array1D[np.uint64],
5958
initial_value=reflection_array,
6059
)
61-
self.crystal_metadata_d_spacing = epics_signal_r(
62-
float, prefix + "DSPACING:RBV"
63-
)
64-
super().__init__(name)
60+
super().__init__(prefix, PitchAndRollCrystal, StationaryCrystal, name)

src/dodal/devices/undulator_dcm.py renamed to src/dodal/devices/i03/undulator_dcm.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@
44
from ophyd_async.core import AsyncStatus, Reference, StandardReadable
55

66
from dodal.common.beamlines.beamline_parameters import get_beamline_parameters
7-
8-
from ..log import LOGGER
9-
from .dcm import DCM
10-
from .undulator import Undulator
7+
from dodal.devices.i03.dcm import DCM
8+
from dodal.devices.undulator import Undulator
9+
from dodal.log import LOGGER
1110

1211
ENERGY_TIMEOUT_S: float = 30.0
1312

@@ -23,6 +22,9 @@ class UndulatorDCM(StandardReadable, Movable[float]):
2322
Calling unulator_dcm.set(energy) will move the DCM motor, perform a table lookup
2423
and move the Undulator gap motor if needed. So the set method can be thought of as
2524
a comprehensive way to set beam energy.
25+
26+
This class will be removed in the future. Use the separate Undulator and DCM devices
27+
instead. See https://github.com/DiamondLightSource/dodal/issues/1092
2628
"""
2729

2830
def __init__(

src/dodal/devices/i22/dcm.py

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,25 @@
55
from event_model.documents.event_descriptor import DataKey
66
from ophyd_async.core import (
77
Array1D,
8-
StandardReadable,
98
StandardReadableFormat,
109
soft_signal_r_and_setter,
1110
)
1211
from ophyd_async.epics.core import epics_signal_r
1312
from ophyd_async.epics.motor import Motor
1413

1514
from dodal.common.crystal_metadata import CrystalMetadata
15+
from dodal.devices.common_dcm import (
16+
BaseDCM,
17+
PitchAndRollCrystal,
18+
RollCrystal,
19+
)
1620

1721
# Conversion constant for energy and wavelength, taken from the X-Ray data booklet
1822
# Converts between energy in KeV and wavelength in angstrom
1923
_CONVERSION_CONSTANT = 12.3984
2024

2125

22-
class DoubleCrystalMonochromator(StandardReadable):
26+
class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
2327
"""
2428
A double crystal monochromator (DCM), used to select the energy of the beam.
2529
@@ -39,13 +43,7 @@ def __init__(
3943
) -> None:
4044
with self.add_children_as_readables():
4145
# Positionable Parameters
42-
self.bragg = Motor(prefix + "BRAGG")
43-
self.offset = Motor(prefix + "OFFSET")
4446
self.perp = Motor(prefix + "PERP")
45-
self.energy = Motor(prefix + "ENERGY")
46-
self.crystal_1_roll = Motor(prefix + "XTAL1:ROLL")
47-
self.crystal_2_roll = Motor(prefix + "XTAL2:ROLL")
48-
self.crystal_2_pitch = Motor(prefix + "XTAL2:PITCH")
4947

5048
# Temperatures
5149
self.backplate_temp = epics_signal_r(float, temperature_prefix + "PT100-7")
@@ -93,12 +91,12 @@ def __init__(
9391
units=crystal_2_metadata.d_spacing[1],
9492
)
9593

96-
super().__init__(name)
94+
super().__init__(prefix, RollCrystal, PitchAndRollCrystal, name)
9795

9896
async def describe(self) -> dict[str, DataKey]:
9997
default_describe = await super().describe()
10098
return {
101-
f"{self.name}-wavelength": DataKey(
99+
f"{self.name}-wavelength_in_a": DataKey(
102100
dtype="number",
103101
shape=[],
104102
source=self.name,
@@ -109,15 +107,15 @@ async def describe(self) -> dict[str, DataKey]:
109107

110108
async def read(self) -> dict[str, Reading]:
111109
default_reading = await super().read()
112-
energy: float = default_reading[f"{self.name}-energy"]["value"]
110+
energy: float = default_reading[f"{self.name}-energy_in_kev"]["value"]
113111
if energy > 0.0:
114112
wavelength = _CONVERSION_CONSTANT / energy
115113
else:
116114
wavelength = 0.0
117115

118116
return {
119117
**default_reading,
120-
f"{self.name}-wavelength": Reading(
118+
f"{self.name}-wavelength_in_a": Reading(
121119
value=wavelength,
122120
timestamp=time.time(),
123121
),

src/dodal/devices/i24/dcm.py

Lines changed: 8 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,19 @@
1-
from ophyd_async.core import StandardReadable
21
from ophyd_async.epics.core import epics_signal_r
3-
from ophyd_async.epics.motor import Motor
42

3+
from dodal.devices.common_dcm import (
4+
BaseDCM,
5+
PitchAndRollCrystal,
6+
RollCrystal,
7+
)
58

6-
class DCM(StandardReadable):
9+
10+
class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
711
"""
812
A double crystal monocromator device, used to select the beam energy.
913
"""
1014

1115
def __init__(self, prefix: str, name: str = "") -> None:
1216
with self.add_children_as_readables():
13-
# Motors
14-
self.bragg_in_degrees = Motor(prefix + "-MO-DCM-01:BRAGG")
15-
self.x_translation_in_mm = Motor(prefix + "-MO-DCM-01:X")
16-
self.offset_in_mm = Motor(prefix + "-MO-DCM-01:OFFSET")
17-
self.gap_in_mm = Motor(prefix + "-MO-DCM-01:GAP")
18-
self.energy_in_kev = Motor(prefix + "-MO-DCM-01:ENERGY")
19-
self.xtal1_roll = Motor(prefix + "-MO-DCM-01:XTAL1:ROLL")
20-
self.xtal2_roll = Motor(prefix + "-MO-DCM-01:XTAL2:ROLL")
21-
self.xtal2_pitch = Motor(prefix + "-MO-DCM-01:XTAL2:PITCH")
22-
23-
# Wavelength is calculated in epics from the energy
24-
self.wavelength_in_a = epics_signal_r(float, prefix + "-MO-DCM-01:LAMBDA")
25-
2617
# Temperatures
2718
self.xtal1_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-1")
2819
self.xtal1_heater_temp = epics_signal_r(
@@ -39,4 +30,4 @@ def __init__(self, prefix: str, name: str = "") -> None:
3930
self.b1_plate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-7")
4031
self.gap_temp = epics_signal_r(float, prefix + "-DI-DCM-01:TC-1")
4132

42-
super().__init__(name)
33+
super().__init__(prefix + "-MO-DCM-01:", RollCrystal, PitchAndRollCrystal, name)

0 commit comments

Comments
 (0)