22
33from ophyd_async .core import (
44 StandardReadable ,
5+ derived_signal_r ,
56)
67from ophyd_async .epics .core import epics_signal_r
78from ophyd_async .epics .motor import Motor
@@ -31,42 +32,39 @@ def __init__(self, prefix):
3132Xtal_2 = TypeVar ("Xtal_2" , bound = StationaryCrystal )
3233
3334
34- class BaseDCM (StandardReadable , Generic [Xtal_1 , Xtal_2 ]):
35+ class DoubleCrystalMonochromatorBase (StandardReadable , Generic [Xtal_1 , Xtal_2 ]):
3536 """
36- Common device for the double crystal monochromator (DCM), used to select the energy of the beam.
37+ Base device for the double crystal monochromator (DCM), used to select the energy of the beam.
3738
3839 Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
3940 each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
4041 This base device accounts for all combinations of this.
4142
42- This device should act as a parent for beamline-specific DCM's, in which any other missing signals can be added.
43+ This device should act as a parent for beamline-specific DCM's which do not match the standard EPICS interface, it provides
44+ only energy and the crystal configuration. Most beamlines should use DoubleCrystalMonochromator instead
4345
4446 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])`
47+ which only requires one crystal which can roll should be typed
48+ 'def my_plan(dcm: DoubleCrystalMonochromatorBase[RollCrystal, StationaryCrystal])`
4649 """
4750
4851 def __init__ (
4952 self , prefix : str , xtal_1 : type [Xtal_1 ], xtal_2 : type [Xtal_2 ], name : str = ""
5053 ) -> None :
5154 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
55+ # Virtual motor PV's which set the physical motors so that the DCM produces requested energy
5456 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"
57+ self .energy_in_ev = derived_signal_r (
58+ self ._convert_keV_to_eV , energy_signal = self .energy_in_kev .user_readback
6459 )
6560
6661 self ._make_crystals (prefix , xtal_1 , xtal_2 )
6762
6863 super ().__init__ (name )
6964
65+ def _convert_keV_to_eV (self , energy_signal : float ) -> float : # noqa: N802
66+ return energy_signal * 1000
67+
7068 # Prefix convention is different depending on whether there are one or two controllable crystals
7169 def _make_crystals (self , prefix : str , xtal_1 : type [Xtal_1 ], xtal_2 : type [Xtal_2 ]):
7270 if StationaryCrystal not in [xtal_1 , xtal_2 ]:
@@ -75,3 +73,52 @@ def _make_crystals(self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2]
7573 else :
7674 self .xtal_1 = xtal_1 (prefix )
7775 self .xtal_2 = xtal_2 (prefix )
76+
77+
78+ class DoubleCrystalMonochromator (
79+ DoubleCrystalMonochromatorBase , Generic [Xtal_1 , Xtal_2 ]
80+ ):
81+ """
82+ Common device for the double crystal monochromator (DCM), used to select the energy of the beam.
83+
84+ Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
85+ each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
86+ This base device accounts for all combinations of this.
87+
88+ This device should act as a parent for beamline-specific DCM's, in which any other missing signals can be added.
89+
90+ Bluesky plans using DCM's should be typed to specify which types of crystals are required. For example, a plan which only
91+ requires one crystal which can roll should be typed 'def my_plan(dcm: DoubleCrystalMonochromator[RollCrystal, StationaryCrystal])`
92+ """
93+
94+ def __init__ (
95+ self , prefix : str , xtal_1 : type [Xtal_1 ], xtal_2 : type [Xtal_2 ], name : str = ""
96+ ) -> None :
97+ super ().__init__ (prefix , xtal_1 , xtal_2 , name )
98+ with self .add_children_as_readables ():
99+ # Virtual motor PV's which set the physical motors so that the DCM produces requested
100+ # wavelength
101+ self .wavelength_in_a = Motor (prefix + "WAVELENGTH" )
102+
103+ # Real motors
104+ self .bragg_in_degrees = Motor (prefix + "BRAGG" )
105+ # Offset ensures that the beam exits the DCM at the same point, regardless of energy.
106+ self .offset_in_mm = Motor (prefix + "OFFSET" )
107+
108+
109+ class DoubleCrystalMonochromatorWithDSpacing (
110+ DoubleCrystalMonochromator , Generic [Xtal_1 , Xtal_2 ]
111+ ):
112+ """
113+ Adds crystal D-spacing metadata to the DoubleCrystalMonochromator class. This should be used in preference to the
114+ DoubleCrystalMonochromator on beamlines which have a "DSPACING:RBV" pv on their DCM.
115+ """
116+
117+ def __init__ (
118+ self , prefix : str , xtal_1 : type [Xtal_1 ], xtal_2 : type [Xtal_2 ], name : str = ""
119+ ) -> None :
120+ super ().__init__ (prefix , xtal_1 , xtal_2 , name )
121+ with self .add_children_as_readables ():
122+ self .crystal_metadata_d_spacing_a = epics_signal_r (
123+ float , prefix + "DSPACING:RBV"
124+ )
0 commit comments