dls-dodal 1.45.0__py3-none-any.whl → 1.47.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/RECORD +76 -64
  3. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/__init__.py +0 -1
  6. dodal/beamlines/b07.py +2 -6
  7. dodal/beamlines/b07_1.py +1 -3
  8. dodal/beamlines/i03.py +16 -19
  9. dodal/beamlines/i04.py +49 -17
  10. dodal/beamlines/i09.py +1 -3
  11. dodal/beamlines/i09_1.py +1 -3
  12. dodal/beamlines/i18.py +7 -4
  13. dodal/beamlines/i22.py +3 -3
  14. dodal/beamlines/i23.py +75 -4
  15. dodal/beamlines/p38.py +4 -4
  16. dodal/beamlines/p60.py +2 -6
  17. dodal/beamlines/p99.py +48 -4
  18. dodal/common/beamlines/beamline_parameters.py +1 -2
  19. dodal/common/beamlines/beamline_utils.py +5 -0
  20. dodal/common/data_util.py +4 -0
  21. dodal/devices/aperturescatterguard.py +47 -47
  22. dodal/devices/common_dcm.py +77 -0
  23. dodal/devices/current_amplifiers/struck_scaler_counter.py +1 -1
  24. dodal/devices/diamond_filter.py +5 -17
  25. dodal/devices/eiger.py +1 -1
  26. dodal/devices/electron_analyser/__init__.py +8 -0
  27. dodal/devices/electron_analyser/abstract/__init__.py +28 -0
  28. dodal/devices/electron_analyser/abstract/base_detector.py +210 -0
  29. dodal/devices/electron_analyser/abstract/base_driver_io.py +121 -0
  30. dodal/devices/electron_analyser/{abstract_region.py → abstract/base_region.py} +2 -9
  31. dodal/devices/electron_analyser/specs/__init__.py +11 -0
  32. dodal/devices/electron_analyser/specs/detector.py +29 -0
  33. dodal/devices/electron_analyser/specs/driver_io.py +64 -0
  34. dodal/devices/electron_analyser/{specs_region.py → specs/region.py} +1 -1
  35. dodal/devices/electron_analyser/types.py +6 -0
  36. dodal/devices/electron_analyser/util.py +13 -0
  37. dodal/devices/electron_analyser/vgscienta/__init__.py +12 -0
  38. dodal/devices/electron_analyser/vgscienta/detector.py +36 -0
  39. dodal/devices/electron_analyser/vgscienta/driver_io.py +39 -0
  40. dodal/devices/electron_analyser/{vgscienta_region.py → vgscienta/region.py} +1 -1
  41. dodal/devices/fast_grid_scan.py +7 -9
  42. dodal/devices/i03/__init__.py +3 -0
  43. dodal/devices/{dcm.py → i03/dcm.py} +8 -12
  44. dodal/devices/{undulator_dcm.py → i03/undulator_dcm.py} +6 -4
  45. dodal/devices/i04/__init__.py +3 -0
  46. dodal/devices/i04/constants.py +9 -0
  47. dodal/devices/i04/murko_results.py +195 -0
  48. dodal/devices/i10/diagnostics.py +9 -61
  49. dodal/devices/i13_1/merlin.py +3 -4
  50. dodal/devices/i13_1/merlin_controller.py +1 -1
  51. dodal/devices/i22/dcm.py +10 -12
  52. dodal/devices/i24/dcm.py +8 -17
  53. dodal/devices/i24/focus_mirrors.py +9 -13
  54. dodal/devices/i24/pilatus_metadata.py +9 -9
  55. dodal/devices/i24/pmac.py +19 -14
  56. dodal/devices/{i03 → mx_phase1}/beamstop.py +6 -12
  57. dodal/devices/oav/oav_calculations.py +2 -2
  58. dodal/devices/oav/oav_detector.py +32 -22
  59. dodal/devices/oav/utils.py +2 -2
  60. dodal/devices/p99/andor2_point.py +41 -0
  61. dodal/devices/positioner.py +49 -0
  62. dodal/devices/tetramm.py +8 -6
  63. dodal/devices/turbo_slit.py +2 -2
  64. dodal/devices/util/adjuster_plans.py +1 -1
  65. dodal/devices/zebra/zebra.py +4 -0
  66. dodal/devices/zebra/zebra_constants_mapping.py +1 -1
  67. dodal/devices/zocalo/__init__.py +0 -3
  68. dodal/devices/zocalo/zocalo_results.py +6 -32
  69. dodal/log.py +14 -14
  70. dodal/plan_stubs/data_session.py +10 -1
  71. dodal/plan_stubs/electron_analyser/__init__.py +3 -0
  72. dodal/plan_stubs/electron_analyser/{configure_controller.py → configure_driver.py} +30 -18
  73. dodal/plans/verify_undulator_gap.py +2 -2
  74. dodal/common/signal_utils.py +0 -88
  75. dodal/devices/electron_analyser/abstract_analyser_io.py +0 -47
  76. dodal/devices/electron_analyser/specs_analyser_io.py +0 -19
  77. dodal/devices/electron_analyser/vgscienta_analyser_io.py +0 -26
  78. dodal/devices/logging_ophyd_device.py +0 -17
  79. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/entry_points.txt +0 -0
  80. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/licenses/LICENSE +0 -0
  81. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,5 @@
1
1
  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
3
3
  from ophyd_async.epics.core import epics_signal_r
4
4
  from ophyd_async.epics.motor import Motor
5
5
 
@@ -8,9 +8,14 @@ from dodal.common.crystal_metadata import (
8
8
  MaterialsEnum,
9
9
  make_crystal_metadata_from_material,
10
10
  )
11
+ from dodal.devices.common_dcm import (
12
+ BaseDCM,
13
+ PitchAndRollCrystal,
14
+ StationaryCrystal,
15
+ )
11
16
 
12
17
 
13
- class DCM(StandardReadable):
18
+ class DCM(BaseDCM[PitchAndRollCrystal, StationaryCrystal]):
14
19
  """
15
20
  A double crystal monochromator (DCM), used to select the energy of the beam.
16
21
 
@@ -30,13 +35,7 @@ class DCM(StandardReadable):
30
35
  MaterialsEnum.Si, (1, 1, 1)
31
36
  )
32
37
  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")
36
38
  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")
40
39
 
41
40
  # temperatures
42
41
  self.xtal1_temp = epics_signal_r(float, prefix + "TEMP1")
@@ -58,7 +57,4 @@ class DCM(StandardReadable):
58
57
  Array1D[np.uint64],
59
58
  initial_value=reflection_array,
60
59
  )
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)
@@ -4,10 +4,9 @@ from bluesky.protocols import Movable
4
4
  from ophyd_async.core import AsyncStatus, Reference, StandardReadable
5
5
 
6
6
  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
11
10
 
12
11
  ENERGY_TIMEOUT_S: float = 30.0
13
12
 
@@ -23,6 +22,9 @@ class UndulatorDCM(StandardReadable, Movable[float]):
23
22
  Calling unulator_dcm.set(energy) will move the DCM motor, perform a table lookup
24
23
  and move the Undulator gap motor if needed. So the set method can be thought of as
25
24
  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
26
28
  """
27
29
 
28
30
  def __init__(
@@ -0,0 +1,3 @@
1
+ from dodal.devices.mx_phase1.beamstop import Beamstop, BeamstopPositions
2
+
3
+ __all__ = ["Beamstop", "BeamstopPositions"]
@@ -0,0 +1,9 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class RedisConstants:
7
+ REDIS_HOST = os.environ.get("VALKEY_PROD_SVC_SERVICE_HOST", "test_redis")
8
+ REDIS_PASSWORD = os.environ.get("VALKEY_PASSWORD", "test_redis_password")
9
+ MURKO_REDIS_DB = 7
@@ -0,0 +1,195 @@
1
+ import json
2
+ import pickle
3
+ from collections import OrderedDict
4
+ from enum import Enum
5
+ from typing import TypedDict
6
+
7
+ import numpy as np
8
+ from bluesky.protocols import Stageable, Triggerable
9
+ from ophyd_async.core import (
10
+ AsyncStatus,
11
+ StandardReadable,
12
+ soft_signal_r_and_setter,
13
+ soft_signal_rw,
14
+ )
15
+ from redis.asyncio import StrictRedis
16
+
17
+ from dodal.devices.i04.constants import RedisConstants
18
+ from dodal.devices.oav.oav_calculations import (
19
+ calculate_beam_distance,
20
+ camera_coordinates_to_xyz_mm,
21
+ )
22
+ from dodal.log import LOGGER
23
+
24
+ MurkoResult = dict
25
+ FullMurkoResults = dict[str, list[MurkoResult]]
26
+
27
+
28
+ class MurkoMetadata(TypedDict):
29
+ zoom_percentage: float
30
+ microns_per_x_pixel: float
31
+ microns_per_y_pixel: float
32
+ beam_centre_i: int
33
+ beam_centre_j: int
34
+ sample_id: str
35
+ omega_angle: float
36
+ uuid: str
37
+
38
+
39
+ class Coord(Enum):
40
+ x = 0
41
+ y = 1
42
+ z = 2
43
+
44
+
45
+ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
46
+ """Device that takes crystal centre coords from Murko and uses them to set the
47
+ x, y, z coordinate of the sample to be in line with the beam centre.
48
+ (x, z) coords can be read at 90°, and (x, y) at 180° (or the closest omega angle to
49
+ 90° and 180°). The average of the x values at these angles is taken, and sin(omega)z
50
+ and cosine(omega)y are taken to account for the rotation. This value is used to
51
+ calculate a number of mm the sample needs to move to be in line with the beam centre.
52
+ """
53
+
54
+ TIMEOUT_S = 2
55
+
56
+ def __init__(
57
+ self,
58
+ redis_host=RedisConstants.REDIS_HOST,
59
+ redis_password=RedisConstants.REDIS_PASSWORD,
60
+ redis_db=RedisConstants.MURKO_REDIS_DB,
61
+ name="",
62
+ ):
63
+ self.redis_client = StrictRedis(
64
+ host=redis_host,
65
+ password=redis_password,
66
+ db=redis_db,
67
+ )
68
+ self.pubsub = self.redis_client.pubsub()
69
+ self._last_omega = 0
70
+ self._last_result = None
71
+ self.sample_id = soft_signal_rw(str) # Should get from redis
72
+ self.coords = {"x": {}, "y": {}, "z": {}}
73
+ self.search_angles = OrderedDict(
74
+ [ # Angles to search and dimensions to gather at each angle
75
+ (90, ("x", "z")),
76
+ (180, ("x", "y")),
77
+ (270, ()), # Stop searching here
78
+ ]
79
+ )
80
+ self.angles_to_search = list(self.search_angles.keys())
81
+
82
+ with self.add_children_as_readables():
83
+ # Diffs from current x/y/z
84
+ self.x_mm, self._x_mm_setter = soft_signal_r_and_setter(float)
85
+ self.y_mm, self._y_mm_setter = soft_signal_r_and_setter(float)
86
+ self.z_mm, self._z_mm_setter = soft_signal_r_and_setter(float)
87
+ super().__init__(name=name)
88
+
89
+ @AsyncStatus.wrap
90
+ async def stage(self):
91
+ await self.pubsub.subscribe("murko-results")
92
+ self._x_mm_setter(0)
93
+ self._y_mm_setter(0)
94
+ self._z_mm_setter(0)
95
+
96
+ @AsyncStatus.wrap
97
+ async def unstage(self):
98
+ await self.pubsub.unsubscribe()
99
+
100
+ @AsyncStatus.wrap
101
+ async def trigger(self):
102
+ # Wait for results
103
+ sample_id = await self.sample_id.get_value()
104
+ final_message = None
105
+ while self.angles_to_search:
106
+ # waits here for next batch to be recieved
107
+ message = await self.pubsub.get_message(timeout=self.TIMEOUT_S)
108
+ if message is None: # No more messages to process
109
+ await self.process_batch(
110
+ final_message, sample_id
111
+ ) # Process final message again
112
+ break
113
+ await self.process_batch(message, sample_id)
114
+ final_message = message
115
+ x_values = list(self.coords["x"].values())
116
+ y_values = list(self.coords["y"].values())
117
+ z_values = list(self.coords["z"].values())
118
+ assert x_values, "No x values"
119
+ assert z_values, "No z values"
120
+ assert y_values, "No y values"
121
+ self._x_mm_setter(float(np.mean(x_values)))
122
+ self._y_mm_setter(float(np.mean(y_values)))
123
+ self._z_mm_setter(float(np.mean(z_values)))
124
+
125
+ async def process_batch(self, message: dict | None, sample_id: str):
126
+ if message and message["type"] == "message":
127
+ batch_results = pickle.loads(message["data"])
128
+ for results in batch_results:
129
+ LOGGER.info(f"Got {results} from redis")
130
+ for uuid, result in results.items():
131
+ metadata_str = await self.redis_client.hget( # type: ignore
132
+ f"murko:{sample_id}:metadata", uuid
133
+ )
134
+ if metadata_str and self.angles_to_search:
135
+ self.process_result(result, uuid, metadata_str)
136
+
137
+ def process_result(
138
+ self, result: dict, uuid: int, metadata_str: str
139
+ ) -> float | None:
140
+ metadata = MurkoMetadata(json.loads(metadata_str))
141
+ omega_angle = metadata["omega_angle"]
142
+ LOGGER.info(f"Got angle {omega_angle}")
143
+ # Find closest to next search angle
144
+ movement = self.get_coords_if_at_angle(metadata, result, omega_angle)
145
+ if movement is not None:
146
+ LOGGER.info(f"Using result {uuid}, {metadata_str}, {result}")
147
+ search_angle = self.angles_to_search.pop(0)
148
+ for coord in self.search_angles[search_angle]:
149
+ self.coords[coord][omega_angle] = movement[Coord[coord].value]
150
+ LOGGER.info(f"Found {coord} at {movement}, angle = {omega_angle}")
151
+ self._last_omega = omega_angle
152
+ self._last_result = result
153
+
154
+ def get_coords_if_at_angle(
155
+ self, metadata: MurkoMetadata, result: MurkoResult, omega: float
156
+ ) -> np.ndarray | None:
157
+ """Gets the 'most_likely_click' coordinates from Murko if omega or the last
158
+ omega are the closest angle to the search angle. Otherwise returns None.
159
+ """
160
+ search_angle = self.angles_to_search[0]
161
+ LOGGER.info(f"Compare {omega}, {search_angle}, {self._last_omega}")
162
+ if ( # if last omega is closest
163
+ abs(omega - search_angle) >= abs(self._last_omega - search_angle)
164
+ and self._last_result is not None
165
+ ):
166
+ closest_result = self._last_result
167
+ closest_omega = self._last_omega
168
+ elif omega - search_angle >= 0: # if this omega is closest
169
+ closest_result = result
170
+ closest_omega = omega
171
+ else:
172
+ return None
173
+ coords = closest_result[
174
+ "most_likely_click"
175
+ ] # As proportion from top, left of image
176
+ shape = closest_result["original_shape"] # Dimensions of image in pixels
177
+ # Murko returns coords as y, x
178
+ centre_px = (coords[1] * shape[1], coords[0] * shape[0])
179
+ LOGGER.info(
180
+ f"Using image taken at {closest_omega}, which found xtal at {centre_px}"
181
+ )
182
+
183
+ beam_dist_px = calculate_beam_distance(
184
+ (metadata["beam_centre_i"], metadata["beam_centre_j"]),
185
+ centre_px[0],
186
+ centre_px[1],
187
+ )
188
+
189
+ return camera_coordinates_to_xyz_mm(
190
+ beam_dist_px[0],
191
+ beam_dist_px[1],
192
+ closest_omega,
193
+ metadata["microns_per_x_pixel"],
194
+ metadata["microns_per_y_pixel"],
195
+ )
@@ -1,6 +1,4 @@
1
- from bluesky.protocols import Movable
2
1
  from ophyd_async.core import (
3
- AsyncStatus,
4
2
  Device,
5
3
  StandardReadable,
6
4
  StrictEnum,
@@ -13,7 +11,6 @@ from ophyd_async.epics.core import (
13
11
  epics_signal_r,
14
12
  epics_signal_rw,
15
13
  )
16
- from ophyd_async.epics.motor import Motor
17
14
 
18
15
  from dodal.devices.current_amplifiers import (
19
16
  CurrentAmpDet,
@@ -23,6 +20,7 @@ from dodal.devices.current_amplifiers import (
23
20
  FemtoDDPCA,
24
21
  StruckScaler,
25
22
  )
23
+ from dodal.devices.positioner import create_positioner
26
24
 
27
25
 
28
26
  class D3Position(StrictEnum):
@@ -70,36 +68,6 @@ class InOutReadBackTable(StrictEnum):
70
68
  OUT_OF_BEAM = "Out of Beam"
71
69
 
72
70
 
73
- class Positioner(StandardReadable, Movable):
74
- """1D stage with a enum table to select positions."""
75
-
76
- def __init__(
77
- self,
78
- prefix: str,
79
- positioner_enum: type[StrictEnum],
80
- positioner_suffix: str = "",
81
- Positioner_pv_suffix: str = ":MP:SELECT",
82
- name: str = "",
83
- ) -> None:
84
- self._stage_motion = Motor(prefix=prefix + positioner_suffix)
85
- with self.add_children_as_readables(Format.CONFIG_SIGNAL):
86
- self.stage_position = epics_signal_rw(
87
- positioner_enum,
88
- read_pv=prefix + positioner_suffix + Positioner_pv_suffix,
89
- )
90
- super().__init__(name=name)
91
- self.positioner_enum = positioner_enum
92
-
93
- @AsyncStatus.wrap
94
- async def set(self, value: StrictEnum) -> None:
95
- if value in self.positioner_enum:
96
- await self.stage_position.set(value=value)
97
- else:
98
- raise ValueError(
99
- f"{value} is not an allow position. Position must be: {self.positioner_enum}"
100
- )
101
-
102
-
103
71
  class I10PneumaticStage(StandardReadable):
104
72
  """Pneumatic stage only has two real positions in or out.
105
73
  Use for fluorescent screen which can be insert into the x-ray beam.
@@ -154,16 +122,13 @@ class FullDiagnostic(Device):
154
122
  self,
155
123
  prefix: str,
156
124
  positioner_enum: type[StrictEnum],
157
- positioner_suffix: str = "",
158
- Positioner_pv_suffix: str = ":MP:SELECT",
125
+ positioner_suffix: str,
159
126
  cam_infix: str = "DCAM:",
160
127
  name: str = "",
161
128
  ) -> None:
162
- self.positioner = Positioner(
163
- prefix=prefix,
164
- positioner_enum=positioner_enum,
165
- positioner_suffix=positioner_suffix,
166
- Positioner_pv_suffix=Positioner_pv_suffix,
129
+ self.positioner = create_positioner(
130
+ positioner_enum,
131
+ prefix + positioner_suffix,
167
132
  )
168
133
  self.screen = ScreenCam(
169
134
  prefix,
@@ -185,28 +150,11 @@ class I10Diagnostic(Device):
185
150
  positioner_suffix="DET:X",
186
151
  )
187
152
  self.d4 = ScreenCam(prefix=prefix + "PHDGN-04:")
188
- self.d5 = Positioner(
189
- prefix=prefix + "IONC-01:",
190
- positioner_enum=D5Position,
191
- positioner_suffix="Y",
192
- )
193
-
194
- self.d5A = Positioner(
195
- prefix=prefix + "PHDGN-06:",
196
- positioner_enum=D5APosition,
197
- positioner_suffix="DET:X",
198
- )
153
+ self.d5 = create_positioner(D5Position, f"{prefix}IONC-01:Y")
154
+ self.d5A = create_positioner(D5APosition, f"{prefix}PHDGN-06:DET:X")
155
+ self.d6 = FullDiagnostic(f"{prefix}PHDGN-05:", D6Position, "DET:X")
156
+ self.d7 = create_positioner(D7Position, f"{prefix}PHDGN-07:Y")
199
157
 
200
- self.d6 = FullDiagnostic(
201
- prefix=prefix + "PHDGN-05:",
202
- positioner_enum=D6Position,
203
- positioner_suffix="DET:X",
204
- )
205
- self.d7 = Positioner(
206
- prefix=prefix + "PHDGN-07:",
207
- positioner_enum=D7Position,
208
- positioner_suffix="Y",
209
- )
210
158
  super().__init__(name)
211
159
 
212
160
 
@@ -23,10 +23,9 @@ class Merlin(StandardDetector):
23
23
  super().__init__(
24
24
  MerlinController(self.drv),
25
25
  adcore.ADHDFWriter(
26
- self.hdf,
27
- path_provider,
28
- lambda: self.name,
29
- adcore.ADBaseDatasetDescriber(self.drv),
26
+ fileio=self.hdf,
27
+ path_provider=path_provider,
28
+ dataset_describer=adcore.ADBaseDatasetDescriber(self.drv),
30
29
  ),
31
30
  config_sigs=(self.drv.acquire_period, self.drv.acquire_time),
32
31
  name=name,
@@ -37,7 +37,7 @@ class MerlinController(ADBaseController):
37
37
  DEFAULT_TIMEOUT + await self.driver.acquire_time.get_value()
38
38
  )
39
39
  await asyncio.gather(
40
- self.driver.num_images.set(trigger_info.total_number_of_triggers),
40
+ self.driver.num_images.set(trigger_info.total_number_of_exposures),
41
41
  self.driver.image_mode.set(ADImageMode.MULTIPLE),
42
42
  )
43
43
 
dodal/devices/i22/dcm.py CHANGED
@@ -5,7 +5,6 @@ from bluesky.protocols import Reading
5
5
  from event_model.documents.event_descriptor import DataKey
6
6
  from ophyd_async.core import (
7
7
  Array1D,
8
- StandardReadable,
9
8
  StandardReadableFormat,
10
9
  soft_signal_r_and_setter,
11
10
  )
@@ -13,13 +12,18 @@ from ophyd_async.epics.core import epics_signal_r
13
12
  from ophyd_async.epics.motor import Motor
14
13
 
15
14
  from dodal.common.crystal_metadata import CrystalMetadata
15
+ from dodal.devices.common_dcm import (
16
+ BaseDCM,
17
+ PitchAndRollCrystal,
18
+ RollCrystal,
19
+ )
16
20
 
17
21
  # Conversion constant for energy and wavelength, taken from the X-Ray data booklet
18
22
  # Converts between energy in KeV and wavelength in angstrom
19
23
  _CONVERSION_CONSTANT = 12.3984
20
24
 
21
25
 
22
- class DoubleCrystalMonochromator(StandardReadable):
26
+ class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
23
27
  """
24
28
  A double crystal monochromator (DCM), used to select the energy of the beam.
25
29
 
@@ -39,13 +43,7 @@ class DoubleCrystalMonochromator(StandardReadable):
39
43
  ) -> None:
40
44
  with self.add_children_as_readables():
41
45
  # Positionable Parameters
42
- self.bragg = Motor(prefix + "BRAGG")
43
- self.offset = Motor(prefix + "OFFSET")
44
46
  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")
49
47
 
50
48
  # Temperatures
51
49
  self.backplate_temp = epics_signal_r(float, temperature_prefix + "PT100-7")
@@ -93,12 +91,12 @@ class DoubleCrystalMonochromator(StandardReadable):
93
91
  units=crystal_2_metadata.d_spacing[1],
94
92
  )
95
93
 
96
- super().__init__(name)
94
+ super().__init__(prefix, RollCrystal, PitchAndRollCrystal, name)
97
95
 
98
96
  async def describe(self) -> dict[str, DataKey]:
99
97
  default_describe = await super().describe()
100
98
  return {
101
- f"{self.name}-wavelength": DataKey(
99
+ f"{self.name}-wavelength_in_a": DataKey(
102
100
  dtype="number",
103
101
  shape=[],
104
102
  source=self.name,
@@ -109,7 +107,7 @@ class DoubleCrystalMonochromator(StandardReadable):
109
107
 
110
108
  async def read(self) -> dict[str, Reading]:
111
109
  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"]
113
111
  if energy > 0.0:
114
112
  wavelength = _CONVERSION_CONSTANT / energy
115
113
  else:
@@ -117,7 +115,7 @@ class DoubleCrystalMonochromator(StandardReadable):
117
115
 
118
116
  return {
119
117
  **default_reading,
120
- f"{self.name}-wavelength": Reading(
118
+ f"{self.name}-wavelength_in_a": Reading(
121
119
  value=wavelength,
122
120
  timestamp=time.time(),
123
121
  ),
dodal/devices/i24/dcm.py CHANGED
@@ -1,28 +1,19 @@
1
- from ophyd_async.core import StandardReadable
2
1
  from ophyd_async.epics.core import epics_signal_r
3
- from ophyd_async.epics.motor import Motor
4
2
 
3
+ from dodal.devices.common_dcm import (
4
+ BaseDCM,
5
+ PitchAndRollCrystal,
6
+ RollCrystal,
7
+ )
5
8
 
6
- class DCM(StandardReadable):
9
+
10
+ class DCM(BaseDCM[RollCrystal, PitchAndRollCrystal]):
7
11
  """
8
12
  A double crystal monocromator device, used to select the beam energy.
9
13
  """
10
14
 
11
15
  def __init__(self, prefix: str, name: str = "") -> None:
12
16
  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
-
26
17
  # Temperatures
27
18
  self.xtal1_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-1")
28
19
  self.xtal1_heater_temp = epics_signal_r(
@@ -39,4 +30,4 @@ class DCM(StandardReadable):
39
30
  self.b1_plate_temp = epics_signal_r(float, prefix + "-DI-DCM-01:PT100-7")
40
31
  self.gap_temp = epics_signal_r(float, prefix + "-DI-DCM-01:TC-1")
41
32
 
42
- super().__init__(name)
33
+ super().__init__(prefix + "-MO-DCM-01:", RollCrystal, PitchAndRollCrystal, name)
@@ -1,8 +1,6 @@
1
- from ophyd_async.core import StandardReadable, StrictEnum
1
+ from ophyd_async.core import StandardReadable, StrictEnum, derived_signal_r
2
2
  from ophyd_async.epics.core import epics_signal_rw
3
3
 
4
- from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
5
-
6
4
 
7
5
  class HFocusMode(StrictEnum):
8
6
  FOCUS_10 = "HMFMfocus10"
@@ -40,21 +38,19 @@ class FocusMirrorsMode(StandardReadable):
40
38
  self.vertical = epics_signal_rw(VFocusMode, prefix + "G0:TARGETAPPLY")
41
39
 
42
40
  with self.add_children_as_readables():
43
- self.beam_size_x = create_r_hardware_backed_soft_signal(
44
- int, self._get_beam_size_x, units="um"
41
+ self.beam_size_x = derived_signal_r(
42
+ self._get_beam_size_x, horizontal=self.horizontal, derived_units="um"
45
43
  )
46
- self.beam_size_y = create_r_hardware_backed_soft_signal(
47
- int, self._get_beam_size_y, units="um"
44
+ self.beam_size_y = derived_signal_r(
45
+ self._get_beam_size_y, vertical=self.vertical, derived_units="um"
48
46
  )
49
47
 
50
48
  super().__init__(name)
51
49
 
52
- async def _get_beam_size_x(self) -> int:
53
- h_mode = await self.horizontal.get_value()
54
- beam_x = BEAM_SIZES[h_mode.removeprefix("HMFM")][0]
50
+ def _get_beam_size_x(self, horizontal: HFocusMode) -> int:
51
+ beam_x = BEAM_SIZES[horizontal.removeprefix("HMFM")][0]
55
52
  return beam_x
56
53
 
57
- async def _get_beam_size_y(self) -> int:
58
- v_mode = await self.vertical.get_value()
59
- beam_y = BEAM_SIZES[v_mode.removeprefix("VMFM")][1]
54
+ def _get_beam_size_y(self, vertical: VFocusMode) -> int:
55
+ beam_y = BEAM_SIZES[vertical.removeprefix("VMFM")][1]
60
56
  return beam_y
@@ -2,11 +2,9 @@
2
2
 
3
3
  import re
4
4
 
5
- from ophyd_async.core import StandardReadable
5
+ from ophyd_async.core import StandardReadable, derived_signal_r
6
6
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
7
7
 
8
- from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
9
-
10
8
 
11
9
  class PilatusMetadata(StandardReadable):
12
10
  def __init__(self, prefix: str, name: str = "") -> None:
@@ -14,21 +12,23 @@ class PilatusMetadata(StandardReadable):
14
12
  self.template = epics_signal_r(str, prefix + "cam1:FileTemplate_RBV")
15
13
  self.filenumber = epics_signal_r(int, prefix + "cam1:FileNumber_RBV")
16
14
  with self.add_children_as_readables():
17
- self.filename_template = create_r_hardware_backed_soft_signal(
18
- str, self._get_full_filename_template
15
+ self.filename_template = derived_signal_r(
16
+ self._get_full_filename_template,
17
+ filename=self.filename,
18
+ filename_template=self.template,
19
+ file_number=self.filenumber,
19
20
  )
20
21
  super().__init__(name)
21
22
 
22
- async def _get_full_filename_template(self) -> str:
23
+ def _get_full_filename_template(
24
+ self, filename: str, filename_template: str, file_number: int
25
+ ) -> str:
23
26
  """
24
27
  Get the template file path by querying the detector PVs.
25
28
  Mirror the construction that the PPU does.
26
29
 
27
30
  Returns: A template string, with the image numbers replaced with '#'
28
31
  """
29
- filename = await self.filename.get_value()
30
- filename_template = await self.template.get_value()
31
- file_number = await self.filenumber.get_value()
32
32
  # Exploit fact that passing negative numbers will put the - before the 0's
33
33
  expected_filename = str(
34
34
  filename_template % (filename, f"{file_number:05d}_", -9)