dls-dodal 1.33.0__py3-none-any.whl → 1.35.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 (89) hide show
  1. {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/METADATA +3 -3
  2. dls_dodal-1.35.0.dist-info/RECORD +147 -0
  3. {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/WHEEL +1 -1
  4. dodal/__init__.py +8 -0
  5. dodal/_version.py +2 -2
  6. dodal/beamline_specific_utils/i03.py +6 -2
  7. dodal/beamlines/__init__.py +2 -3
  8. dodal/beamlines/i03.py +41 -9
  9. dodal/beamlines/i04.py +26 -4
  10. dodal/beamlines/i10.py +257 -0
  11. dodal/beamlines/i22.py +25 -13
  12. dodal/beamlines/i24.py +11 -11
  13. dodal/beamlines/p38.py +24 -13
  14. dodal/common/beamlines/beamline_utils.py +1 -2
  15. dodal/common/crystal_metadata.py +61 -0
  16. dodal/common/signal_utils.py +10 -14
  17. dodal/common/types.py +2 -7
  18. dodal/devices/CTAB.py +1 -1
  19. dodal/devices/aperture.py +1 -1
  20. dodal/devices/aperturescatterguard.py +20 -8
  21. dodal/devices/apple2_undulator.py +603 -0
  22. dodal/devices/areadetector/plugins/CAM.py +29 -0
  23. dodal/devices/areadetector/plugins/MJPG.py +51 -106
  24. dodal/devices/attenuator.py +1 -1
  25. dodal/devices/backlight.py +11 -11
  26. dodal/devices/cryostream.py +3 -5
  27. dodal/devices/dcm.py +26 -2
  28. dodal/devices/detector/detector_motion.py +3 -5
  29. dodal/devices/diamond_filter.py +46 -0
  30. dodal/devices/eiger.py +6 -2
  31. dodal/devices/eiger_odin.py +48 -39
  32. dodal/devices/fast_grid_scan.py +1 -1
  33. dodal/devices/fluorescence_detector_motion.py +5 -7
  34. dodal/devices/focusing_mirror.py +26 -19
  35. dodal/devices/hutch_shutter.py +4 -5
  36. dodal/devices/i10/i10_apple2.py +399 -0
  37. dodal/devices/i10/i10_setting_data.py +7 -0
  38. dodal/devices/i22/dcm.py +50 -83
  39. dodal/devices/i22/fswitch.py +5 -5
  40. dodal/devices/i24/aperture.py +3 -5
  41. dodal/devices/i24/beamstop.py +3 -5
  42. dodal/devices/i24/dcm.py +1 -1
  43. dodal/devices/i24/dual_backlight.py +9 -11
  44. dodal/devices/i24/pmac.py +35 -46
  45. dodal/devices/i24/vgonio.py +16 -0
  46. dodal/devices/ipin.py +5 -3
  47. dodal/devices/linkam3.py +7 -7
  48. dodal/devices/oav/oav_calculations.py +22 -0
  49. dodal/devices/oav/oav_detector.py +118 -83
  50. dodal/devices/oav/oav_parameters.py +50 -104
  51. dodal/devices/oav/oav_to_redis_forwarder.py +77 -35
  52. dodal/devices/oav/pin_image_recognition/__init__.py +9 -7
  53. dodal/devices/oav/{grid_overlay.py → snapshots/grid_overlay.py} +16 -59
  54. dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +64 -0
  55. dodal/devices/oav/snapshots/snapshot_with_grid.py +57 -0
  56. dodal/devices/oav/utils.py +28 -27
  57. dodal/devices/p99/sample_stage.py +3 -5
  58. dodal/devices/pgm.py +40 -0
  59. dodal/devices/qbpm.py +18 -0
  60. dodal/devices/robot.py +5 -5
  61. dodal/devices/smargon.py +3 -3
  62. dodal/devices/synchrotron.py +9 -4
  63. dodal/devices/tetramm.py +9 -9
  64. dodal/devices/thawer.py +13 -7
  65. dodal/devices/undulator.py +7 -6
  66. dodal/devices/util/adjuster_plans.py +1 -1
  67. dodal/devices/util/epics_util.py +1 -1
  68. dodal/devices/util/lookup_tables.py +4 -5
  69. dodal/devices/watsonmarlow323_pump.py +45 -0
  70. dodal/devices/webcam.py +9 -2
  71. dodal/devices/xbpm_feedback.py +3 -5
  72. dodal/devices/xspress3/xspress3.py +8 -9
  73. dodal/devices/xspress3/xspress3_channel.py +3 -5
  74. dodal/devices/zebra.py +12 -8
  75. dodal/devices/zebra_controlled_shutter.py +5 -6
  76. dodal/devices/zocalo/__init__.py +2 -2
  77. dodal/devices/zocalo/zocalo_constants.py +3 -0
  78. dodal/devices/zocalo/zocalo_interaction.py +2 -1
  79. dodal/devices/zocalo/zocalo_results.py +105 -89
  80. dodal/plans/data_session_metadata.py +2 -2
  81. dodal/plans/motor_util_plans.py +11 -9
  82. dodal/utils.py +11 -0
  83. dls_dodal-1.33.0.dist-info/RECORD +0 -136
  84. dodal/beamlines/i04_1.py +0 -140
  85. dodal/devices/i24/i24_vgonio.py +0 -17
  86. dodal/devices/oav/oav_errors.py +0 -35
  87. {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/LICENSE +0 -0
  88. {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/entry_points.txt +0 -0
  89. {dls_dodal-1.33.0.dist-info → dls_dodal-1.35.0.dist-info}/top_level.txt +0 -0
@@ -1,13 +1,12 @@
1
- from enum import Enum
2
-
3
1
  from bluesky.protocols import Movable
4
2
  from ophyd_async.core import (
5
3
  DEFAULT_TIMEOUT,
6
4
  AsyncStatus,
7
5
  StandardReadable,
6
+ StrictEnum,
8
7
  wait_for_value,
9
8
  )
10
- from ophyd_async.epics.signal import epics_signal_r, epics_signal_w
9
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_w
11
10
 
12
11
  HUTCH_SAFE_FOR_OPERATIONS = 0 # Hutch is locked and can't be entered
13
12
 
@@ -16,13 +15,13 @@ class ShutterNotSafeToOperateError(Exception):
16
15
  pass
17
16
 
18
17
 
19
- class ShutterDemand(str, Enum):
18
+ class ShutterDemand(StrictEnum):
20
19
  OPEN = "Open"
21
20
  CLOSE = "Close"
22
21
  RESET = "Reset"
23
22
 
24
23
 
25
- class ShutterState(str, Enum):
24
+ class ShutterState(StrictEnum):
26
25
  FAULT = "Fault"
27
26
  OPEN = "Open"
28
27
  OPENING = "Opening"
@@ -0,0 +1,399 @@
1
+ import asyncio
2
+ import csv
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ from typing import Any, SupportsFloat
6
+
7
+ import numpy as np
8
+ from bluesky.protocols import Movable
9
+ from ophyd_async.core import (
10
+ AsyncStatus,
11
+ Reference,
12
+ StandardReadable,
13
+ StandardReadableFormat,
14
+ soft_signal_r_and_setter,
15
+ soft_signal_rw,
16
+ )
17
+
18
+ from dodal.devices.apple2_undulator import (
19
+ Apple2,
20
+ Apple2Val,
21
+ Lookuptable,
22
+ UndulatorGap,
23
+ UndulatorJawPhase,
24
+ UndulatorPhaseAxes,
25
+ )
26
+ from dodal.devices.pgm import PGM
27
+ from dodal.log import LOGGER
28
+
29
+ ROW_PHASE_MOTOR_TOLERANCE = 0.004
30
+ MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
31
+ MAXIMUM_GAP_MOTOR_POSITION = 100
32
+ DEFAULT_JAW_PHASE_POLY_PARAMS = [1.0 / 7.5, -120.0 / 7.5]
33
+ ALPHA_OFFSET = 180
34
+
35
+
36
+ # data class to store the lookup table configuration that is use in convert_csv_to_lookup
37
+ @dataclass
38
+ class LookupPath:
39
+ Gap: Path
40
+ Phase: Path
41
+
42
+
43
+ @dataclass
44
+ class LookupTableConfig:
45
+ path: LookupPath
46
+ source: tuple[str, str]
47
+ mode: str | None
48
+ min_energy: str | None
49
+ max_energy: str | None
50
+ poly_deg: list | None
51
+
52
+
53
+ class I10Apple2(Apple2):
54
+ """
55
+ I10Apple2 is the i10 version of Apple2 ID.
56
+ The set and update_lookuptable should be the only part that is I10 specific.
57
+
58
+ A pair of look up tables are needed to provide the conversion
59
+ between motor position and energy.
60
+ Set is in energy(eV).
61
+ """
62
+
63
+ def __init__(
64
+ self,
65
+ id_gap: UndulatorGap,
66
+ id_phase: UndulatorPhaseAxes,
67
+ id_jaw_phase: UndulatorJawPhase,
68
+ energy_gap_table_path: Path,
69
+ energy_phase_table_path: Path,
70
+ source: tuple[str, str],
71
+ prefix: str = "",
72
+ mode: str = "Mode",
73
+ min_energy: str = "MinEnergy",
74
+ max_energy: str = "MaxEnergy",
75
+ poly_deg: list | None = None,
76
+ name: str = "",
77
+ ) -> None:
78
+ """
79
+ Parameters
80
+ ----------
81
+ id_gap:
82
+ An UndulatorGap device.
83
+ id_phase:
84
+ An UndulatorPhaseAxes device.
85
+ energy_gap_table_path:
86
+ The path to id gap look up table.
87
+ energy_phase_table_path:
88
+ The path to id phase look up table.
89
+ source:
90
+ The column name and the name of the source in look up table. e.g. ("source", "idu")
91
+ mode:
92
+ The column name of the mode in look up table.
93
+ min_energy:
94
+ The column name that contain the maximum energy in look up table.
95
+ max_energy:
96
+ The column name that contain the maximum energy in look up table.
97
+ poly_deg:
98
+ The column names for the parameters for the energy conversion polynomial, starting with the least significant.
99
+ prefix:
100
+ Not in use but needed for device_instantiation.
101
+ Name:
102
+ Name of the device
103
+ """
104
+
105
+ # A dataclass contains the path to the look up table and the expected column names.
106
+ self.lookup_table_config = LookupTableConfig(
107
+ path=LookupPath(Gap=energy_gap_table_path, Phase=energy_phase_table_path),
108
+ source=source,
109
+ mode=mode,
110
+ min_energy=min_energy,
111
+ max_energy=max_energy,
112
+ poly_deg=poly_deg,
113
+ )
114
+
115
+ super().__init__(
116
+ id_gap=id_gap,
117
+ id_phase=id_phase,
118
+ prefix=prefix,
119
+ name=name,
120
+ )
121
+ with self.add_children_as_readables():
122
+ self.id_jaw_phase = Reference(id_jaw_phase)
123
+
124
+ @AsyncStatus.wrap
125
+ async def set(self, value: SupportsFloat) -> None:
126
+ """
127
+ Check polarisation state and use it together with the energy(value)
128
+ to calculate the required gap and phases before setting it.
129
+ """
130
+ value = float(value)
131
+ if self.pol is None:
132
+ LOGGER.warning("Polarisation not set attempting to read from hardware")
133
+ pol, phase = await self.determinePhaseFromHardware()
134
+ if pol is None:
135
+ raise ValueError(f"Pol is not set for {self.name}")
136
+ self.pol = pol
137
+
138
+ self._polarisation_set(self.pol)
139
+ gap, phase = self._get_id_gap_phase(value)
140
+ phase3 = phase * (-1 if self.pol == "la" else (1))
141
+ id_set_val = Apple2Val(
142
+ top_outer=str(phase),
143
+ top_inner="0.0",
144
+ btm_inner=str(phase3),
145
+ btm_outer="0.0",
146
+ gap=str(gap),
147
+ )
148
+ LOGGER.info(f"Setting polarisation to {self.pol}, with {id_set_val}")
149
+ await self._set(value=id_set_val, energy=value)
150
+ if self.pol != "la":
151
+ await self.id_jaw_phase().set(0)
152
+ await self.id_jaw_phase().set_move.set(1)
153
+
154
+ def update_lookuptable(self):
155
+ """
156
+ Update the stored lookup tabled from file.
157
+
158
+ """
159
+ LOGGER.info("Updating lookup dictionary from file.")
160
+ for key, path in self.lookup_table_config.path.__dict__.items():
161
+ if path.exists():
162
+ self.lookup_tables[key] = convert_csv_to_lookup(
163
+ file=path,
164
+ source=self.lookup_table_config.source,
165
+ mode=self.lookup_table_config.mode,
166
+ min_energy=self.lookup_table_config.min_energy,
167
+ max_energy=self.lookup_table_config.max_energy,
168
+ poly_deg=self.lookup_table_config.poly_deg,
169
+ )
170
+ # ensure the importing lookup table is the correct format
171
+ Lookuptable.model_validate(self.lookup_tables[key])
172
+ else:
173
+ raise FileNotFoundError(f"{key} look up table is not in path: {path}")
174
+
175
+ self._available_pol = list(self.lookup_tables["Gap"].keys())
176
+
177
+
178
+ class I10Apple2PGM(StandardReadable, Movable):
179
+ """
180
+ Compound device to set both ID and PGM energy at the sample time,poly_deg
181
+
182
+ """
183
+
184
+ def __init__(
185
+ self, id: I10Apple2, pgm: PGM, prefix: str = "", name: str = ""
186
+ ) -> None:
187
+ """
188
+ Parameters
189
+ ----------
190
+ id:
191
+ An Apple2 device.
192
+ pgm:
193
+ A PGM/mono device.
194
+ prefix:
195
+ Not in use but needed for device_instantiation.
196
+ name:
197
+ New device name.
198
+ """
199
+ super().__init__(name=name)
200
+ self.id_ref = Reference(id)
201
+ self.pgm_ref = Reference(pgm)
202
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
203
+ self.energy_offset = soft_signal_rw(float, initial_value=0)
204
+
205
+ @AsyncStatus.wrap
206
+ async def set(self, value: float) -> None:
207
+ LOGGER.info(f"Moving f{self.name} energy to {value}.")
208
+ await asyncio.gather(
209
+ self.id_ref().set(value=value + await self.energy_offset.get_value()),
210
+ self.pgm_ref().energy.set(value),
211
+ )
212
+
213
+
214
+ class I10Apple2Pol(StandardReadable, Movable):
215
+ """
216
+ Compound device to set polorisation of ID.
217
+ """
218
+
219
+ def __init__(self, id: I10Apple2, prefix: str = "", name: str = "") -> None:
220
+ """
221
+ Parameters
222
+ ----------
223
+ id:
224
+ An I10Apple2 device.
225
+ prefix:
226
+ Not in use but needed for device_instantiation.
227
+ name:
228
+ New device name.
229
+ """
230
+ super().__init__(name=name)
231
+ with self.add_children_as_readables():
232
+ self.id = id
233
+
234
+ @AsyncStatus.wrap
235
+ async def set(self, value: str) -> None:
236
+ self.id.pol = value # change polarisation.
237
+ LOGGER.info(f"Changing f{self.name} polarisation to {value}.")
238
+ await self.id.set(
239
+ await self.id.energy.get_value()
240
+ ) # Move id to new polarisation
241
+
242
+
243
+ class LinearArbitraryAngle(StandardReadable, Movable):
244
+ """
245
+ Device to set polorisation angle of the ID. Linear Arbitrary Angle (laa)
246
+ is the direction of the magnetic field which can be change by varying the jaw_phase
247
+ in (linear arbitrary (la) mode,
248
+ The angle of 0 is equivalent to linear horizontal "lh" (sigma) and
249
+ 90 is linear vertical "lv" (pi).
250
+ This device require a jaw_phase to angle conversion which is done via a polynomial.
251
+ """
252
+
253
+ def __init__(
254
+ self,
255
+ id: I10Apple2,
256
+ prefix: str = "",
257
+ name: str = "",
258
+ jaw_phase_limit: float = 12.0,
259
+ jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
260
+ angle_threshold_deg=30.0,
261
+ ) -> None:
262
+ """
263
+ Parameters
264
+ ----------
265
+ id: I10Apple2
266
+ An I10Apple2 device.
267
+ prefix: str
268
+ Not in use but needed for device_instantiation.
269
+ name: str
270
+ New device name.
271
+ jaw_phase_limit: float
272
+ The maximum allowed jaw_phase movement.
273
+ jaw_phase_poly_param: list
274
+ polynomial parameters highest power first.
275
+ """
276
+ super().__init__(name=name)
277
+ self.id_ref = Reference(id)
278
+ self.jaw_phase_from_angle = np.poly1d(jaw_phase_poly_param)
279
+ self.angle_threshold_deg = angle_threshold_deg
280
+ self.jaw_phase_limit = jaw_phase_limit
281
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
282
+ self.angle, self._angle_set = soft_signal_r_and_setter(
283
+ float, initial_value=None
284
+ )
285
+
286
+ @AsyncStatus.wrap
287
+ async def set(self, value: SupportsFloat) -> None:
288
+ value = float(value)
289
+ pol = self.id_ref().pol
290
+ if pol != "la":
291
+ raise RuntimeError(
292
+ f"Angle control is not available in polarisation {pol} with {self.id_ref().name}"
293
+ )
294
+ # Moving to real angle which is 210 to 30.
295
+ alpha_real = value if value > self.angle_threshold_deg else value + ALPHA_OFFSET
296
+ jaw_phase = self.jaw_phase_from_angle(alpha_real)
297
+ if abs(jaw_phase) > self.jaw_phase_limit:
298
+ raise RuntimeError(
299
+ f"jaw_phase position for angle ({value}) is outside permitted range"
300
+ f" [-{self.jaw_phase_limit}, {self.jaw_phase_limit}]"
301
+ )
302
+ await self.id_ref().id_jaw_phase().set(jaw_phase)
303
+ self._angle_set(value)
304
+
305
+
306
+ def convert_csv_to_lookup(
307
+ file: str,
308
+ source: tuple[str, str],
309
+ mode: str | None = "Mode",
310
+ min_energy: str | None = "MinEnergy",
311
+ max_energy: str | None = "MaxEnergy",
312
+ poly_deg: list | None = None,
313
+ ) -> dict[str | None, dict[str, dict[str, dict[str, Any]]]]:
314
+ """
315
+ Convert csv to a dictionary that can be read by Apple2 ID device.
316
+
317
+ Parameters
318
+ -----------
319
+ file: str
320
+ File path.
321
+ source: tuple[str, str]
322
+ Tuple(column name, source name)
323
+ e.g. ("Source", "idu").
324
+ mode: str = "Mode"
325
+ Column name for the available modes, "lv","lh","pc","nc" etc
326
+ min_energy: str = "MinEnergy":
327
+ Column name for min energy for the polynomial.
328
+ max_energy: str = "MaxEnergy",
329
+ Column name for max energy for the polynomial.
330
+ poly_deg: list | None = None,
331
+ Column names for the parameters for the polynomial, starting with the least significant.
332
+
333
+ return
334
+ ------
335
+ return a dictionary that conform to Apple2 lookup table format:
336
+
337
+ {mode: {'Energies': {Any: {'Low': float,
338
+ 'High': float,
339
+ 'Poly':np.poly1d
340
+ }
341
+ }
342
+ 'Limit': {'Minimum': float,
343
+ 'Maximum': float
344
+ }
345
+ }
346
+ }
347
+ """
348
+ if poly_deg is None:
349
+ poly_deg = [
350
+ "7th-order",
351
+ "6th-order",
352
+ "5th-order",
353
+ "4th-order",
354
+ "3rd-order",
355
+ "2nd-order",
356
+ "1st-order",
357
+ "b",
358
+ ]
359
+ look_up_table = {}
360
+ pol = []
361
+
362
+ def data2dict(row) -> None:
363
+ # logic for the conversion for each row of data.
364
+ if row[mode] not in pol:
365
+ pol.append(row[mode])
366
+ look_up_table[row[mode]] = {}
367
+ look_up_table[row[mode]] = {
368
+ "Energies": {},
369
+ "Limit": {
370
+ "Minimum": float(row[min_energy]),
371
+ "Maximum": float(row[max_energy]),
372
+ },
373
+ }
374
+
375
+ # create polynomial object for energy to gap/phase
376
+ cof = [float(row[x]) for x in poly_deg]
377
+ poly = np.poly1d(cof)
378
+
379
+ look_up_table[row[mode]]["Energies"][row[min_energy]] = {
380
+ "Low": float(row[min_energy]),
381
+ "High": float(row[max_energy]),
382
+ "Poly": poly,
383
+ }
384
+ look_up_table[row[mode]]["Limit"]["Minimum"] = min(
385
+ look_up_table[row[mode]]["Limit"]["Minimum"], float(row[min_energy])
386
+ )
387
+ look_up_table[row[mode]]["Limit"]["Maximum"] = max(
388
+ look_up_table[row[mode]]["Limit"]["Maximum"], float(row[max_energy])
389
+ )
390
+
391
+ with open(file, newline="") as csvfile:
392
+ reader = csv.DictReader(csvfile)
393
+ for row in reader:
394
+ # If there are multiple source only convert requested.
395
+ if row[source[0]] == source[1]:
396
+ data2dict(row=row)
397
+ if not look_up_table:
398
+ raise RuntimeError(f"Unable to convert lookup table:/n/t{file}")
399
+ return look_up_table
@@ -0,0 +1,7 @@
1
+ from ophyd_async.core import StrictEnum
2
+
3
+
4
+ class I10Grating(StrictEnum):
5
+ Au400 = "400 line/mm Au"
6
+ Si400 = "400 line/mm Si"
7
+ Au1200 = "1200 line/mm Au"
dodal/devices/i22/dcm.py CHANGED
@@ -1,32 +1,24 @@
1
1
  import time
2
- from collections.abc import Sequence
3
- from dataclasses import dataclass
4
- from typing import Literal
5
2
 
3
+ import numpy as np
6
4
  from bluesky.protocols import Reading
7
5
  from event_model.documents.event_descriptor import DataKey
8
- from ophyd_async.core import ConfigSignal, StandardReadable, soft_signal_r_and_setter
6
+ from ophyd_async.core import (
7
+ Array1D,
8
+ StandardReadable,
9
+ StandardReadableFormat,
10
+ soft_signal_r_and_setter,
11
+ )
12
+ from ophyd_async.epics.core import epics_signal_r
9
13
  from ophyd_async.epics.motor import Motor
10
- from ophyd_async.epics.signal import epics_signal_r
14
+
15
+ from dodal.common.crystal_metadata import CrystalMetadata
11
16
 
12
17
  # Conversion constant for energy and wavelength, taken from the X-Ray data booklet
13
18
  # Converts between energy in KeV and wavelength in angstrom
14
19
  _CONVERSION_CONSTANT = 12.3984
15
20
 
16
21
 
17
- @dataclass(frozen=True, unsafe_hash=True)
18
- class CrystalMetadata:
19
- """
20
- Metadata used in the NeXus format,
21
- see https://manual.nexusformat.org/classes/base_classes/NXcrystal.html
22
- """
23
-
24
- usage: Literal["Bragg", "Laue"] | None = None
25
- type: str | None = None
26
- reflection: tuple[int, int, int] | None = None
27
- d_spacing: tuple[float, str] | None = None
28
-
29
-
30
22
  class DoubleCrystalMonochromator(StandardReadable):
31
23
  """
32
24
  A double crystal monochromator (DCM), used to select the energy of the beam.
@@ -39,22 +31,21 @@ class DoubleCrystalMonochromator(StandardReadable):
39
31
 
40
32
  def __init__(
41
33
  self,
42
- motion_prefix: str,
43
34
  temperature_prefix: str,
44
- crystal_1_metadata: CrystalMetadata | None = None,
45
- crystal_2_metadata: CrystalMetadata | None = None,
35
+ crystal_1_metadata: CrystalMetadata,
36
+ crystal_2_metadata: CrystalMetadata,
46
37
  prefix: str = "",
47
38
  name: str = "",
48
39
  ) -> None:
49
40
  with self.add_children_as_readables():
50
41
  # Positionable Parameters
51
- self.bragg = Motor(motion_prefix + "BRAGG")
52
- self.offset = Motor(motion_prefix + "OFFSET")
53
- self.perp = Motor(motion_prefix + "PERP")
54
- self.energy = Motor(motion_prefix + "ENERGY")
55
- self.crystal_1_roll = Motor(motion_prefix + "XTAL1:ROLL")
56
- self.crystal_2_roll = Motor(motion_prefix + "XTAL2:ROLL")
57
- self.crystal_2_pitch = Motor(motion_prefix + "XTAL2:PITCH")
42
+ self.bragg = Motor(prefix + "BRAGG")
43
+ self.offset = Motor(prefix + "OFFSET")
44
+ 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")
58
49
 
59
50
  # Temperatures
60
51
  self.backplate_temp = epics_signal_r(float, temperature_prefix + "PT100-7")
@@ -70,61 +61,37 @@ class DoubleCrystalMonochromator(StandardReadable):
70
61
 
71
62
  # Soft metadata
72
63
  # If supplied include crystal details in output of read_configuration
73
- crystal_1_metadata = crystal_1_metadata or CrystalMetadata()
74
- crystal_2_metadata = crystal_2_metadata or CrystalMetadata()
75
- with self.add_children_as_readables(ConfigSignal):
76
- if crystal_1_metadata.usage is not None:
77
- self.crystal_1_usage, _ = soft_signal_r_and_setter(
78
- str, initial_value=crystal_1_metadata.usage
79
- )
80
- else:
81
- self.crystal_1_usage = None
82
- if crystal_1_metadata.type is not None:
83
- self.crystal_1_type, _ = soft_signal_r_and_setter(
84
- str, initial_value=crystal_1_metadata.type
85
- )
86
- else:
87
- self.crystal_1_type = None
88
- if crystal_1_metadata.reflection is not None:
89
- self.crystal_1_reflection, _ = soft_signal_r_and_setter(
90
- Sequence[int], initial_value=list(crystal_1_metadata.reflection)
91
- )
92
- else:
93
- self.crystal_1_reflection = None
94
- if crystal_1_metadata.d_spacing is not None:
95
- self.crystal_1_d_spacing, _ = soft_signal_r_and_setter(
96
- float,
97
- initial_value=crystal_1_metadata.d_spacing[0],
98
- units=crystal_1_metadata.d_spacing[1],
99
- )
100
- else:
101
- self.crystal_1_d_spacing = None
102
- if crystal_2_metadata.usage is not None:
103
- self.crystal_2_usage, _ = soft_signal_r_and_setter(
104
- str, initial_value=crystal_2_metadata.usage
105
- )
106
- else:
107
- self.crystal_2_usage = None
108
- if crystal_2_metadata.type is not None:
109
- self.crystal_2_type, _ = soft_signal_r_and_setter(
110
- str, initial_value=crystal_2_metadata.type
111
- )
112
- else:
113
- self.crystal_2_type = None
114
- if crystal_2_metadata.reflection is not None:
115
- self.crystal_2_reflection, _ = soft_signal_r_and_setter(
116
- Sequence[int], initial_value=list(crystal_2_metadata.reflection)
117
- )
118
- else:
119
- self.crystal_2_reflection = None
120
- if crystal_2_metadata.d_spacing is not None:
121
- self.crystal_2_d_spacing, _ = soft_signal_r_and_setter(
122
- float,
123
- initial_value=crystal_2_metadata.d_spacing[0],
124
- units=crystal_2_metadata.d_spacing[1],
125
- )
126
- else:
127
- self.crystal_2_d_spacing = None
64
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
65
+ self.crystal_1_usage, _ = soft_signal_r_and_setter(
66
+ str, initial_value=crystal_1_metadata.usage
67
+ )
68
+ self.crystal_1_type, _ = soft_signal_r_and_setter(
69
+ str, initial_value=crystal_1_metadata.type
70
+ )
71
+ self.crystal_1_reflection, _ = soft_signal_r_and_setter(
72
+ Array1D[np.int32],
73
+ initial_value=np.array(crystal_1_metadata.reflection),
74
+ )
75
+ self.crystal_1_d_spacing, _ = soft_signal_r_and_setter(
76
+ float,
77
+ initial_value=crystal_1_metadata.d_spacing[0],
78
+ units=crystal_1_metadata.d_spacing[1],
79
+ )
80
+ self.crystal_2_usage, _ = soft_signal_r_and_setter(
81
+ str, initial_value=crystal_2_metadata.usage
82
+ )
83
+ self.crystal_2_type, _ = soft_signal_r_and_setter(
84
+ str, initial_value=crystal_2_metadata.type
85
+ )
86
+ self.crystal_2_reflection, _ = soft_signal_r_and_setter(
87
+ Array1D[np.int32],
88
+ initial_value=np.array(crystal_2_metadata.reflection),
89
+ )
90
+ self.crystal_2_d_spacing, _ = soft_signal_r_and_setter(
91
+ float,
92
+ initial_value=crystal_2_metadata.d_spacing[0],
93
+ units=crystal_2_metadata.d_spacing[1],
94
+ )
128
95
 
129
96
  super().__init__(name)
130
97
 
@@ -1,19 +1,19 @@
1
1
  import asyncio
2
2
  import time
3
- from enum import Enum
4
3
 
5
4
  from bluesky.protocols import Reading
6
5
  from event_model import DataKey
7
6
  from ophyd_async.core import (
8
- ConfigSignal,
9
7
  DeviceVector,
10
8
  StandardReadable,
9
+ StandardReadableFormat,
10
+ StrictEnum,
11
11
  soft_signal_r_and_setter,
12
12
  )
13
- from ophyd_async.epics.signal import epics_signal_r
13
+ from ophyd_async.epics.core import epics_signal_r
14
14
 
15
15
 
16
- class FilterState(str, Enum):
16
+ class FilterState(StrictEnum):
17
17
  """
18
18
  Note that the in/out here refers to the internal rocker
19
19
  position so a PV value of IN implies a filter OUT of beam
@@ -54,7 +54,7 @@ class FSwitch(StandardReadable):
54
54
  for i in range(FSwitch.NUM_FILTERS)
55
55
  }
56
56
  )
57
- with self.add_children_as_readables(ConfigSignal):
57
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
58
58
  if lens_geometry is not None:
59
59
  self.lens_geometry, _ = soft_signal_r_and_setter(
60
60
  str, initial_value=lens_geometry
@@ -1,11 +1,9 @@
1
- from enum import Enum
2
-
3
- from ophyd_async.core import StandardReadable
1
+ from ophyd_async.core import StandardReadable, StrictEnum
2
+ from ophyd_async.epics.core import epics_signal_rw
4
3
  from ophyd_async.epics.motor import Motor
5
- from ophyd_async.epics.signal import epics_signal_rw
6
4
 
7
5
 
8
- class AperturePositions(str, Enum):
6
+ class AperturePositions(StrictEnum):
9
7
  IN = "In"
10
8
  OUT = "Out"
11
9
  ROBOT = "Robot"
@@ -1,11 +1,9 @@
1
- from enum import Enum
2
-
3
- from ophyd_async.core import StandardReadable
1
+ from ophyd_async.core import StandardReadable, StrictEnum
2
+ from ophyd_async.epics.core import epics_signal_rw
4
3
  from ophyd_async.epics.motor import Motor
5
- from ophyd_async.epics.signal import epics_signal_rw
6
4
 
7
5
 
8
- class BeamstopPositions(str, Enum):
6
+ class BeamstopPositions(StrictEnum):
9
7
  CHECK_BEAM = "CheckBeam"
10
8
  DATA_COLLECTION = "Data Collection"
11
9
  DATA_COLLECTION_FAR = "Data Collection Far"
dodal/devices/i24/dcm.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from ophyd_async.core import StandardReadable
2
+ from ophyd_async.epics.core import epics_signal_r
2
3
  from ophyd_async.epics.motor import Motor
3
- from ophyd_async.epics.signal import epics_signal_r
4
4
 
5
5
 
6
6
  class DCM(StandardReadable):