Skip to content

Commit 8bb309b

Browse files
DG-At-DiamondfajinyuanVilltordDominicOramoliwenmandiamond
authored
Make DSpacing metadata optional (#1601)
* Make DSpacing metadata optional * Refactor to rename and separate common and base DCM * Update src/dodal/devices/common_dcm.py Co-authored-by: Victor Rogalev <[email protected]> * Update src/dodal/beamlines/i18.py Co-authored-by: Victor Rogalev <[email protected]> * Test fixes for DCMs * Return to separate dcm with d spacing * Move energy conversion tests from i09 in to common test * Tests fixed * Typo correction Co-authored-by: Dominic Oram <[email protected]> * Comment update, also removed dspacing from i15 dcm * Add exception for method name ruff rule for method containing SI units * Update tests/devices/electron_analyser/conftest.py Co-authored-by: oliwenmandiamond <[email protected]> * Remove bad import --------- Co-authored-by: Fajin Yuan <[email protected]> Co-authored-by: Victor Rogalev <[email protected]> Co-authored-by: Dominic Oram <[email protected]> Co-authored-by: oliwenmandiamond <[email protected]>
1 parent 79804a6 commit 8bb309b

File tree

17 files changed

+169
-187
lines changed

17 files changed

+169
-187
lines changed

src/dodal/beamlines/i09.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22
device_factory,
33
)
44
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
5+
from dodal.devices.common_dcm import (
6+
DoubleCrystalMonochromatorWithDSpacing,
7+
PitchAndRollCrystal,
8+
StationaryCrystal,
9+
)
510
from dodal.devices.electron_analyser import DualEnergySource
611
from dodal.devices.electron_analyser.vgscienta import VGScientaDetector
7-
from dodal.devices.i09 import DCM, Grating, LensMode, PassEnergy, PsuMode
12+
from dodal.devices.i09 import Grating, LensMode, PassEnergy, PsuMode
813
from dodal.devices.pgm import PGM
914
from dodal.devices.synchrotron import Synchrotron
1015
from dodal.log import set_beamline as set_log_beamline
@@ -30,8 +35,10 @@ def pgm() -> PGM:
3035

3136

3237
@device_factory()
33-
def dcm() -> DCM:
34-
return DCM(prefix=f"{PREFIX.beamline_prefix}-MO-DCM-01:")
38+
def dcm() -> DoubleCrystalMonochromatorWithDSpacing:
39+
return DoubleCrystalMonochromatorWithDSpacing(
40+
f"{PREFIX.beamline_prefix}-MO-DCM-01:", PitchAndRollCrystal, StationaryCrystal
41+
)
3542

3643

3744
@device_factory()

src/dodal/beamlines/i09_1.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22
device_factory,
33
)
44
from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
5+
from dodal.devices.common_dcm import (
6+
DoubleCrystalMonochromatorWithDSpacing,
7+
PitchAndRollCrystal,
8+
StationaryCrystal,
9+
)
510
from dodal.devices.electron_analyser import EnergySource
611
from dodal.devices.electron_analyser.specs import SpecsDetector
7-
from dodal.devices.i09.dcm import DCM
812
from dodal.devices.i09_1 import LensMode, PsuMode
913
from dodal.devices.synchrotron import Synchrotron
1014
from dodal.log import set_beamline as set_log_beamline
@@ -22,8 +26,10 @@ def synchrotron() -> Synchrotron:
2226

2327

2428
@device_factory()
25-
def dcm() -> DCM:
26-
return DCM(prefix=f"{PREFIX.beamline_prefix}-MO-DCM-01:")
29+
def dcm() -> DoubleCrystalMonochromatorWithDSpacing:
30+
return DoubleCrystalMonochromatorWithDSpacing(
31+
f"{PREFIX.beamline_prefix}-MO-DCM-01:", PitchAndRollCrystal, StationaryCrystal
32+
)
2733

2834

2935
@device_factory()

src/dodal/beamlines/i18.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,11 @@
1212
LocalDirectoryServiceClient,
1313
StaticVisitPathProvider,
1414
)
15-
from dodal.devices.common_dcm import BaseDCM, PitchAndRollCrystal, RollCrystal
15+
from dodal.devices.common_dcm import (
16+
DoubleCrystalMonochromatorWithDSpacing,
17+
PitchAndRollCrystal,
18+
RollCrystal,
19+
)
1620
from dodal.devices.i18.diode import Diode
1721
from dodal.devices.i18.kb_mirror import KBMirror
1822
from dodal.devices.motors import XYStage, XYZThetaStage
@@ -55,13 +59,15 @@ def undulator() -> Undulator:
5559

5660
# See https://github.com/DiamondLightSource/dodal/issues/1180
5761
@device_factory(skip=True)
58-
def dcm() -> BaseDCM[RollCrystal, PitchAndRollCrystal]:
59-
# once spacing is added Si111 d-spacing is 3.135 angsterm , and Si311 is 1.637
60-
# calculations are in gda/config/lookupTables/Si111/eV_Deg_converter.xml
61-
return BaseDCM(
62-
prefix=f"{PREFIX.beamline_prefix}-MO-DCM-01:",
63-
xtal_1=RollCrystal,
64-
xtal_2=PitchAndRollCrystal,
62+
def dcm() -> DoubleCrystalMonochromatorWithDSpacing:
63+
"""
64+
A double crystal monocromator device, used to select the beam energy.
65+
66+
Once spacing is added Si111 d-spacing is 3.135 angsterm , and Si311 is 1.637
67+
calculations are in gda/config/lookupTables/Si111/eV_Deg_converter.xml
68+
"""
69+
return DoubleCrystalMonochromatorWithDSpacing(
70+
f"{PREFIX.beamline_prefix}-MO-DCM-01:", RollCrystal, PitchAndRollCrystal
6571
)
6672

6773

src/dodal/devices/common_dcm.py

Lines changed: 62 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from ophyd_async.core import (
44
StandardReadable,
5+
derived_signal_r,
56
)
67
from ophyd_async.epics.core import epics_signal_r
78
from ophyd_async.epics.motor import Motor
@@ -31,42 +32,39 @@ def __init__(self, prefix):
3132
Xtal_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+
)

src/dodal/devices/i03/dcm.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
make_crystal_metadata_from_material,
1010
)
1111
from dodal.devices.common_dcm import (
12-
BaseDCM,
12+
DoubleCrystalMonochromatorWithDSpacing,
1313
PitchAndRollCrystal,
1414
StationaryCrystal,
1515
)
1616

1717

18-
class DCM(BaseDCM[PitchAndRollCrystal, StationaryCrystal]):
18+
class DCM(
19+
DoubleCrystalMonochromatorWithDSpacing[PitchAndRollCrystal, StationaryCrystal]
20+
):
1921
"""
2022
A double crystal monochromator (DCM), used to select the energy of the beam.
2123

src/dodal/devices/i09/__init__.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
from dodal.devices.i09.dcm import DCM
21
from dodal.devices.i09.enums import Grating, LensMode, PassEnergy, PsuMode
32

4-
__all__ = ["DCM", "Grating", "LensMode", "PsuMode", "PassEnergy"]
3+
__all__ = ["Grating", "LensMode", "PsuMode", "PassEnergy"]

src/dodal/devices/i09/dcm.py

Lines changed: 0 additions & 26 deletions
This file was deleted.

src/dodal/devices/i15/dcm.py

Lines changed: 6 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
from typing import Generic, TypeVar
2-
3-
from ophyd_async.core import StandardReadable
41
from ophyd_async.epics.motor import Motor
52

63
from dodal.devices.common_dcm import (
4+
DoubleCrystalMonochromatorBase,
75
StationaryCrystal,
86
)
97

@@ -24,50 +22,13 @@ def __init__(self, prefix):
2422
super().__init__(prefix)
2523

2624

27-
Xtal_1 = TypeVar("Xtal_1", bound=StationaryCrystal)
28-
Xtal_2 = TypeVar("Xtal_2", bound=StationaryCrystal)
29-
30-
31-
class BaseDCMforI15(StandardReadable, Generic[Xtal_1, Xtal_2]):
32-
"""
33-
Device for double crystal monochromators (DCM), which only allow energy of the beam to be selected.
34-
35-
Features common across all DCM's should include virtual motors to set energy/wavelength and contain two crystals,
36-
each of which can be movable. Some DCM's contain crystals with roll motors, and some contain crystals with roll and pitch motors.
37-
This device only accounts for combinations of energy plus two crystals.
38-
39-
This device is designed to be a drop in replacement for BaseDCM for i15, which doesn't require WAVELENGTH, BRAGG and OFFSET to
40-
be available. Once the i15 DCM supports all of the PVs required by BaseDCM, the i15 DCM device can switch to inheriting from
41-
BaseDCM and this class can be removed.
42-
"""
43-
44-
def __init__(
45-
self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2], name: str = ""
46-
) -> None:
47-
with self.add_children_as_readables():
48-
# Virtual motor PV's which set the physical motors so that the DCM produces requested
49-
# wavelength/energy
50-
self.energy_in_kev = Motor(prefix + "ENERGY")
51-
self._make_crystals(prefix, xtal_1, xtal_2)
52-
53-
super().__init__(name)
54-
55-
# Prefix convention is different depending on whether there are one or two controllable crystals
56-
def _make_crystals(self, prefix: str, xtal_1: type[Xtal_1], xtal_2: type[Xtal_2]):
57-
if StationaryCrystal not in [xtal_1, xtal_2]:
58-
self.xtal_1 = xtal_1(f"{prefix}XTAL1:")
59-
self.xtal_2 = xtal_2(f"{prefix}XTAL2:")
60-
else:
61-
self.xtal_1 = xtal_1(prefix)
62-
self.xtal_2 = xtal_2(prefix)
63-
64-
65-
class DCM(BaseDCMforI15[ThetaRollYZCrystal, ThetaYCrystal]):
25+
class DCM(DoubleCrystalMonochromatorBase[ThetaRollYZCrystal, ThetaYCrystal]):
6626
"""
67-
A double crystal monocromator device, used to select the beam energy.
27+
A double crystal monochromator device, used to select the beam energy.
6828
69-
Once the i15 DCM supports all of the PVs required by BaseDCM, this class can be
70-
changed to inherit from BaseDCM and BaseDCMforI15 can be removed.
29+
Once the i15 DCM supports all of the PVs required by DoubleCrystalMonochromator or
30+
DoubleCrystalMonochromatorWithDSpacing this class can be changed to inherit from it,
31+
see https://jira.diamond.ac.uk/browse/I15-1053 for more info.
7132
"""
7233

7334
def __init__(self, prefix: str, name: str = "") -> None:

src/dodal/devices/i22/dcm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313

1414
from dodal.common.crystal_metadata import CrystalMetadata
1515
from dodal.devices.common_dcm import (
16-
BaseDCM,
16+
DoubleCrystalMonochromatorWithDSpacing,
1717
PitchAndRollCrystal,
1818
RollCrystal,
1919
)
@@ -23,7 +23,7 @@
2323
_CONVERSION_CONSTANT = 12.3984
2424

2525

26-
class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
26+
class DCM(DoubleCrystalMonochromatorWithDSpacing[RollCrystal, PitchAndRollCrystal]):
2727
"""
2828
A double crystal monochromator (DCM), used to select the energy of the beam.
2929

src/dodal/devices/i24/dcm.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
from ophyd_async.epics.core import epics_signal_r
22

33
from dodal.devices.common_dcm import (
4-
BaseDCM,
4+
DoubleCrystalMonochromatorWithDSpacing,
55
PitchAndRollCrystal,
66
RollCrystal,
77
)
88

99

10-
class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
10+
class DCM(DoubleCrystalMonochromatorWithDSpacing[RollCrystal, PitchAndRollCrystal]):
1111
"""
1212
A double crystal monocromator device, used to select the beam energy.
1313
"""

0 commit comments

Comments
 (0)