dls-dodal 1.47.0__py3-none-any.whl → 1.49.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 (61) hide show
  1. {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/METADATA +3 -2
  2. {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/RECORD +59 -49
  3. {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/aithre.py +21 -0
  6. dodal/beamlines/b01_1.py +1 -1
  7. dodal/beamlines/b16.py +65 -0
  8. dodal/beamlines/b18.py +38 -0
  9. dodal/beamlines/i03.py +21 -6
  10. dodal/beamlines/i04.py +17 -10
  11. dodal/beamlines/i10.py +41 -233
  12. dodal/beamlines/i18.py +1 -1
  13. dodal/beamlines/i19_1.py +9 -6
  14. dodal/beamlines/i24.py +5 -5
  15. dodal/beamlines/k11.py +35 -0
  16. dodal/common/beamlines/beamline_parameters.py +2 -28
  17. dodal/common/beamlines/device_helpers.py +1 -0
  18. dodal/devices/aithre_lasershaping/goniometer.py +36 -2
  19. dodal/devices/aithre_lasershaping/laser_robot.py +27 -0
  20. dodal/devices/apple2_undulator.py +257 -136
  21. dodal/devices/b16/__init__.py +0 -0
  22. dodal/devices/b16/detector.py +34 -0
  23. dodal/devices/bimorph_mirror.py +29 -36
  24. dodal/devices/electron_analyser/__init__.py +21 -1
  25. dodal/devices/electron_analyser/abstract/__init__.py +0 -6
  26. dodal/devices/electron_analyser/abstract/base_detector.py +16 -128
  27. dodal/devices/electron_analyser/abstract/base_driver_io.py +122 -8
  28. dodal/devices/electron_analyser/abstract/base_region.py +7 -3
  29. dodal/devices/electron_analyser/detector.py +141 -0
  30. dodal/devices/electron_analyser/enums.py +6 -0
  31. dodal/devices/electron_analyser/specs/__init__.py +3 -2
  32. dodal/devices/electron_analyser/specs/detector.py +6 -22
  33. dodal/devices/electron_analyser/specs/driver_io.py +27 -3
  34. dodal/devices/electron_analyser/specs/enums.py +8 -0
  35. dodal/devices/electron_analyser/specs/region.py +3 -2
  36. dodal/devices/electron_analyser/types.py +30 -4
  37. dodal/devices/electron_analyser/util.py +1 -1
  38. dodal/devices/electron_analyser/vgscienta/__init__.py +3 -2
  39. dodal/devices/electron_analyser/vgscienta/detector.py +9 -23
  40. dodal/devices/electron_analyser/vgscienta/driver_io.py +33 -4
  41. dodal/devices/electron_analyser/vgscienta/enums.py +19 -0
  42. dodal/devices/electron_analyser/vgscienta/region.py +7 -23
  43. dodal/devices/fast_grid_scan.py +1 -1
  44. dodal/devices/i04/murko_results.py +93 -96
  45. dodal/devices/i10/__init__.py +0 -0
  46. dodal/devices/i10/i10_apple2.py +181 -126
  47. dodal/devices/i18/diode.py +37 -4
  48. dodal/devices/i22/nxsas.py +1 -1
  49. dodal/devices/mx_phase1/beamstop.py +23 -6
  50. dodal/devices/oav/oav_detector.py +101 -25
  51. dodal/devices/oav/oav_parameters.py +46 -16
  52. dodal/devices/oav/oav_to_redis_forwarder.py +2 -2
  53. dodal/devices/robot.py +20 -1
  54. dodal/devices/smargon.py +43 -4
  55. dodal/devices/zebra/zebra.py +8 -0
  56. dodal/plans/configure_arm_trigger_and_disarm_detector.py +167 -0
  57. dodal/plan_stubs/electron_analyser/__init__.py +0 -3
  58. dodal/plan_stubs/electron_analyser/configure_driver.py +0 -92
  59. {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/entry_points.txt +0 -0
  60. {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/licenses/LICENSE +0 -0
  61. {dls_dodal-1.47.0.dist-info → dls_dodal-1.49.0.dist-info}/top_level.txt +0 -0
@@ -8,6 +8,7 @@ import numpy as np
8
8
  from bluesky.protocols import Movable
9
9
  from ophyd_async.core import (
10
10
  AsyncStatus,
11
+ Device,
11
12
  Reference,
12
13
  StandardReadable,
13
14
  StandardReadableFormat,
@@ -15,16 +16,18 @@ from ophyd_async.core import (
15
16
  soft_signal_rw,
16
17
  )
17
18
 
18
- from dodal.devices.apple2_undulator import (
19
+ from dodal.log import LOGGER
20
+
21
+ from ..apple2_undulator import (
19
22
  Apple2,
20
23
  Apple2Val,
21
24
  Lookuptable,
25
+ Pol,
22
26
  UndulatorGap,
23
27
  UndulatorJawPhase,
24
28
  UndulatorPhaseAxes,
25
29
  )
26
- from dodal.devices.pgm import PGM
27
- from dodal.log import LOGGER
30
+ from ..pgm import PGM
28
31
 
29
32
  ROW_PHASE_MOTOR_TOLERANCE = 0.004
30
33
  MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
@@ -51,22 +54,20 @@ class LookupTableConfig:
51
54
 
52
55
 
53
56
  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
+ """I10Apple2 is the i10 version of Apple2 ID, set and update_lookuptable function
58
+ should be the only part that is I10 specific.
59
+
60
+ A pair of look up tables are needed to provide the conversion betwApple 2 ID/undulator has 4 extra degrees of freedom compare to the standard Undulator,
61
+ each bank of magnet can move independently to each other,
62
+ which allow the production of different x-ray polarisation as well as energy.
63
+ This type of ID is use on I10, I21, I09, I17 and I06 for soft x-ray.een motor position and energy.
57
64
 
58
- A pair of look up tables are needed to provide the conversion
59
- between motor position and energy.
60
65
  Set is in energy(eV).
61
66
  """
62
67
 
63
68
  def __init__(
64
69
  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
+ look_up_table_dir: str,
70
71
  source: tuple[str, str],
71
72
  prefix: str = "",
72
73
  mode: str = "Mode",
@@ -78,14 +79,8 @@ class I10Apple2(Apple2):
78
79
  """
79
80
  Parameters
80
81
  ----------
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.
82
+ look_up_table_dir:
83
+ The path to look up table.
89
84
  source:
90
85
  The column name and the name of the source in look up table. e.g. ("source", "idu")
91
86
  mode:
@@ -97,11 +92,17 @@ class I10Apple2(Apple2):
97
92
  poly_deg:
98
93
  The column names for the parameters for the energy conversion polynomial, starting with the least significant.
99
94
  prefix:
100
- Not in use but needed for device_instantiation.
95
+ epic pv for id
101
96
  Name:
102
97
  Name of the device
103
98
  """
104
99
 
100
+ energy_gap_table_path = Path(
101
+ look_up_table_dir + "IDEnergy2GapCalibrations.csv",
102
+ )
103
+ energy_phase_table_path = Path(
104
+ look_up_table_dir + "IDEnergy2PhaseCalibrations.csv",
105
+ )
105
106
  # A dataclass contains the path to the look up table and the expected column names.
106
107
  self.lookup_table_config = LookupTableConfig(
107
108
  path=LookupPath(Gap=energy_gap_table_path, Phase=energy_phase_table_path),
@@ -112,44 +113,62 @@ class I10Apple2(Apple2):
112
113
  poly_deg=poly_deg,
113
114
  )
114
115
 
115
- super().__init__(
116
- id_gap=id_gap,
117
- id_phase=id_phase,
118
- prefix=prefix,
119
- name=name,
120
- )
121
116
  with self.add_children_as_readables():
122
- self.id_jaw_phase = Reference(id_jaw_phase)
117
+ super().__init__(
118
+ id_gap=UndulatorGap(name="id_gap", prefix=prefix),
119
+ id_phase=UndulatorPhaseAxes(
120
+ name="id_phase",
121
+ prefix=prefix,
122
+ top_outer="RPQ1",
123
+ top_inner="RPQ2",
124
+ btm_inner="RPQ3",
125
+ btm_outer="RPQ4",
126
+ ),
127
+ prefix=prefix,
128
+ name=name,
129
+ )
130
+ self.id_jaw_phase = UndulatorJawPhase(
131
+ prefix=prefix,
132
+ move_pv="RPQ1",
133
+ )
123
134
 
124
135
  @AsyncStatus.wrap
125
- async def set(self, value: SupportsFloat) -> None:
136
+ async def set(self, value: float) -> None:
126
137
  """
127
138
  Check polarisation state and use it together with the energy(value)
128
139
  to calculate the required gap and phases before setting it.
129
140
  """
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
+
142
+ pol = await self.polarisation_setpoint.get_value()
143
+
144
+ if pol == Pol.NONE:
145
+ LOGGER.warning(
146
+ "Found no setpoint for polarisation. Attempting to"
147
+ " determine polarisation from hardware..."
148
+ )
149
+ pol = await self.polarisation.get_value()
150
+ if pol == Pol.NONE:
151
+ raise ValueError(
152
+ f"Polarisation cannot be determined from hardware for {self.name}"
153
+ )
154
+
155
+ self._set_pol_setpoint(pol)
156
+ gap, phase = await self._get_id_gap_phase(value)
157
+ phase3 = phase * (-1 if pol == Pol.LA else 1)
141
158
  id_set_val = Apple2Val(
142
- top_outer=str(phase),
159
+ top_outer=f"{phase:.6f}",
143
160
  top_inner="0.0",
144
- btm_inner=str(phase3),
161
+ btm_inner=f"{phase3:.6f}",
145
162
  btm_outer="0.0",
146
- gap=str(gap),
163
+ gap=f"{gap:.6f}",
147
164
  )
148
- LOGGER.info(f"Setting polarisation to {self.pol}, with {id_set_val}")
165
+
166
+ LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
149
167
  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)
168
+ if pol != Pol.LA:
169
+ await self.id_jaw_phase.set(0)
170
+ await self.id_jaw_phase.set_move.set(1)
171
+ LOGGER.info(f"Energy set to {value} eV successfully.")
153
172
 
154
173
  def update_lookuptable(self):
155
174
  """
@@ -175,15 +194,13 @@ class I10Apple2(Apple2):
175
194
  self._available_pol = list(self.lookup_tables["Gap"].keys())
176
195
 
177
196
 
178
- class I10Apple2PGM(StandardReadable, Movable[float]):
197
+ class EnergySetter(StandardReadable, Movable[float]):
179
198
  """
180
- Compound device to set both ID and PGM energy at the sample time,poly_deg
199
+ Compound device to set both ID and PGM energy at the same time.
181
200
 
182
201
  """
183
202
 
184
- def __init__(
185
- self, id: I10Apple2, pgm: PGM, prefix: str = "", name: str = ""
186
- ) -> None:
203
+ def __init__(self, id: I10Apple2, pgm: PGM, name: str = "") -> None:
187
204
  """
188
205
  Parameters
189
206
  ----------
@@ -191,53 +208,52 @@ class I10Apple2PGM(StandardReadable, Movable[float]):
191
208
  An Apple2 device.
192
209
  pgm:
193
210
  A PGM/mono device.
194
- prefix:
195
- Not in use but needed for device_instantiation.
196
211
  name:
197
212
  New device name.
198
213
  """
199
214
  super().__init__(name=name)
200
- self.id_ref = Reference(id)
215
+ self.id = id
201
216
  self.pgm_ref = Reference(pgm)
202
- with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
217
+
218
+ self.add_readables(
219
+ [self.id.energy, self.pgm_ref().energy.user_readback],
220
+ StandardReadableFormat.HINTED_SIGNAL,
221
+ )
222
+
223
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
203
224
  self.energy_offset = soft_signal_rw(float, initial_value=0)
204
225
 
205
226
  @AsyncStatus.wrap
206
227
  async def set(self, value: float) -> None:
207
228
  LOGGER.info(f"Moving f{self.name} energy to {value}.")
208
229
  await asyncio.gather(
209
- self.id_ref().set(value=value + await self.energy_offset.get_value()),
230
+ self.id.set(value=value + await self.energy_offset.get_value()),
210
231
  self.pgm_ref().energy.set(value),
211
232
  )
212
233
 
213
234
 
214
- class I10Apple2Pol(StandardReadable, Movable[str]):
235
+ class I10Apple2Pol(StandardReadable, Movable[Pol]):
215
236
  """
216
237
  Compound device to set polorisation of ID.
217
238
  """
218
239
 
219
- def __init__(self, id: I10Apple2, prefix: str = "", name: str = "") -> None:
240
+ def __init__(self, id: I10Apple2, name: str = "") -> None:
220
241
  """
221
242
  Parameters
222
243
  ----------
223
244
  id:
224
245
  An I10Apple2 device.
225
- prefix:
226
- Not in use but needed for device_instantiation.
227
246
  name:
228
247
  New device name.
229
248
  """
230
249
  super().__init__(name=name)
231
- with self.add_children_as_readables():
232
- self.id = id
250
+ self.id_ref = Reference(id)
251
+ self.add_readables([self.id_ref().polarisation])
233
252
 
234
253
  @AsyncStatus.wrap
235
- async def set(self, value: str) -> None:
236
- self.id.pol = value # change polarisation.
254
+ async def set(self, value: Pol) -> None:
237
255
  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
256
+ await self.id_ref().polarisation.set(value)
241
257
 
242
258
 
243
259
  class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
@@ -253,7 +269,6 @@ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
253
269
  def __init__(
254
270
  self,
255
271
  id: I10Apple2,
256
- prefix: str = "",
257
272
  name: str = "",
258
273
  jaw_phase_limit: float = 12.0,
259
274
  jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
@@ -264,8 +279,6 @@ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
264
279
  ----------
265
280
  id: I10Apple2
266
281
  An I10Apple2 device.
267
- prefix: str
268
- Not in use but needed for device_instantiation.
269
282
  name: str
270
283
  New device name.
271
284
  jaw_phase_limit: float
@@ -286,8 +299,8 @@ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
286
299
  @AsyncStatus.wrap
287
300
  async def set(self, value: SupportsFloat) -> None:
288
301
  value = float(value)
289
- pol = self.id_ref().pol
290
- if pol != "la":
302
+ pol = await self.id_ref().polarisation.get_value()
303
+ if pol != Pol.LA:
291
304
  raise RuntimeError(
292
305
  f"Angle control is not available in polarisation {pol} with {self.id_ref().name}"
293
306
  )
@@ -299,10 +312,60 @@ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
299
312
  f"jaw_phase position for angle ({value}) is outside permitted range"
300
313
  f" [-{self.jaw_phase_limit}, {self.jaw_phase_limit}]"
301
314
  )
302
- await self.id_ref().id_jaw_phase().set(jaw_phase)
315
+ await self.id_ref().id_jaw_phase.set(jaw_phase)
303
316
  self._angle_set(value)
304
317
 
305
318
 
319
+ class I10Id(Device):
320
+ def __init__(
321
+ self,
322
+ pgm: PGM,
323
+ prefix: str,
324
+ look_up_table_dir: str,
325
+ source: tuple[str, str],
326
+ jaw_phase_limit=12.0,
327
+ jaw_phase_poly_param=DEFAULT_JAW_PHASE_POLY_PARAMS,
328
+ angle_threshold_deg=30.0,
329
+ name: str = "",
330
+ ) -> None:
331
+ """I10Id is a compound device that combines the I10-specific Apple2 undulator,
332
+ energy setter, and polarization control.
333
+ This class provides a high-level interface for controlling the undulator's
334
+ energy, polarization, and linear arbitrary angle.
335
+
336
+ Attributes
337
+ ----------
338
+ id : I10Apple2
339
+ The I10-specific Apple2 undulator device.
340
+ energy_setter : EnergySetter
341
+ A device for synchronizing the undulator and monochromator energy.
342
+ pol : I10Apple2Pol
343
+ A device for controlling the polarization of the undulator.
344
+ linear_arbitrary_angle : LinearArbitraryAngle
345
+ A device for controlling the linear arbitrary polarization angle.
346
+ """
347
+ self.energy = EnergySetter(
348
+ id=I10Apple2(
349
+ look_up_table_dir=look_up_table_dir,
350
+ name="id_energy",
351
+ source=source,
352
+ prefix=prefix,
353
+ ),
354
+ pgm=pgm,
355
+ name="energy",
356
+ )
357
+ self.pol = I10Apple2Pol(id=self.energy.id, name="pol")
358
+ self.laa = LinearArbitraryAngle(
359
+ id=self.energy.id,
360
+ name="laa",
361
+ jaw_phase_limit=jaw_phase_limit,
362
+ jaw_phase_poly_param=jaw_phase_poly_param,
363
+ angle_threshold_deg=angle_threshold_deg,
364
+ )
365
+
366
+ super().__init__(name=name)
367
+
368
+
306
369
  def convert_csv_to_lookup(
307
370
  file: str,
308
371
  source: tuple[str, str],
@@ -312,38 +375,28 @@ def convert_csv_to_lookup(
312
375
  poly_deg: list | None = None,
313
376
  ) -> dict[str | None, dict[str, dict[str, dict[str, Any]]]]:
314
377
  """
315
- Convert csv to a dictionary that can be read by Apple2 ID device.
378
+ Convert a CSV file to a dictionary compatible with the Apple2 lookup table format.
316
379
 
317
380
  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
- }
381
+ ----------
382
+ file : str
383
+ Path to the CSV file.
384
+ source : tuple[str, str]
385
+ Tuple specifying the column name and source name (e.g., ("Source", "idu")).
386
+ mode : str, optional
387
+ Column name for the available modes (e.g., "lv", "lh", "pc", "nc"), by default "Mode".
388
+ min_energy : str, optional
389
+ Column name for the minimum energy, by default "MinEnergy".
390
+ max_energy : str, optional
391
+ Column name for the maximum energy, by default "MaxEnergy".
392
+ poly_deg : list, optional
393
+ Column names for polynomial coefficients, starting with the least significant term.
394
+
395
+ Returns
396
+ -------
397
+ dict
398
+ A dictionary conforming to the Apple2 lookup table format.
399
+
347
400
  """
348
401
  if poly_deg is None:
349
402
  poly_deg = [
@@ -356,15 +409,15 @@ def convert_csv_to_lookup(
356
409
  "1st-order",
357
410
  "b",
358
411
  ]
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]] = {
412
+ lookup_table = {}
413
+ polarisations = set()
414
+
415
+ def process_row(row: dict) -> None:
416
+ """Process a single row from the CSV file and update the lookup table."""
417
+ mode_value = row[mode]
418
+ if mode_value not in polarisations:
419
+ polarisations.add(mode_value)
420
+ lookup_table[mode_value] = {
368
421
  "Energies": {},
369
422
  "Limit": {
370
423
  "Minimum": float(row[min_energy]),
@@ -372,20 +425,22 @@ def convert_csv_to_lookup(
372
425
  },
373
426
  }
374
427
 
375
- # create polynomial object for energy to gap/phase
376
- cof = [float(row[x]) for x in poly_deg]
377
- poly = np.poly1d(cof)
428
+ # Create polynomial object for energy-to-gap/phase conversion
429
+ coefficients = [float(row[coef]) for coef in poly_deg]
430
+ polynomial = np.poly1d(coefficients)
378
431
 
379
- look_up_table[row[mode]]["Energies"][row[min_energy]] = {
432
+ lookup_table[mode_value]["Energies"][row[min_energy]] = {
380
433
  "Low": float(row[min_energy]),
381
434
  "High": float(row[max_energy]),
382
- "Poly": poly,
435
+ "Poly": polynomial,
383
436
  }
384
- look_up_table[row[mode]]["Limit"]["Minimum"] = min(
385
- look_up_table[row[mode]]["Limit"]["Minimum"], float(row[min_energy])
437
+
438
+ # Update energy limits
439
+ lookup_table[mode_value]["Limit"]["Minimum"] = min(
440
+ lookup_table[mode_value]["Limit"]["Minimum"], float(row[min_energy])
386
441
  )
387
- look_up_table[row[mode]]["Limit"]["Maximum"] = max(
388
- look_up_table[row[mode]]["Limit"]["Maximum"], float(row[max_energy])
442
+ lookup_table[mode_value]["Limit"]["Maximum"] = max(
443
+ lookup_table[mode_value]["Limit"]["Maximum"], float(row[max_energy])
389
444
  )
390
445
 
391
446
  with open(file, newline="") as csvfile:
@@ -393,7 +448,7 @@ def convert_csv_to_lookup(
393
448
  for row in reader:
394
449
  # If there are multiple source only convert requested.
395
450
  if row[source[0]] == source[1]:
396
- data2dict(row=row)
397
- if not look_up_table:
451
+ process_row(row=row)
452
+ if not lookup_table:
398
453
  raise RuntimeError(f"Unable to convert lookup table:/n/t{file}")
399
- return look_up_table
454
+ return lookup_table
@@ -1,8 +1,36 @@
1
- from ophyd_async.core import (
2
- StandardReadable,
3
- )
1
+ from ophyd_async.core import StandardReadable, StrictEnum
4
2
  from ophyd_async.epics.core import epics_signal_r
5
3
 
4
+ from dodal.devices.positioner import create_positioner
5
+
6
+
7
+ class FilterAValues(StrictEnum):
8
+ """Maps from a short usable name to the string name in EPICS"""
9
+
10
+ AL_2MM = "2 mm Al"
11
+ AL_1_5MM = "1.5 mm Al"
12
+ AL_1_25MM = "1.25 mm Al"
13
+ AL_0_8MM = "0.8 mm Al"
14
+ AL_0_55MM = "0.55 mm Al"
15
+ AL_0_5MM = "0.5 mm Al"
16
+ AL_0_3MM = "0.3 mm Al"
17
+ AL_0_25MM = "0.25 mm Al"
18
+ AL_0_15MM = "0.15 mm Al"
19
+ AL_0_1MM = "0.1 mm Al"
20
+ AL_0_05MM = "0.05 mm Al"
21
+ AL_0_025MM = "0.025 mm Al"
22
+ AL_GAP = "Gap"
23
+
24
+
25
+ class FilterBValues(StrictEnum):
26
+ DIAMOND_THIN = "Diamond thin"
27
+ DIAMOND_THICK = "Diamond thick"
28
+ NI_DRAIN = "ni drain"
29
+ AU_DRAIN = "au drain"
30
+ AL_DRAIN = "al drain"
31
+ GAP = "Gap"
32
+ IN_LINE_DIODE = "in line diode"
33
+
6
34
 
7
35
  class Diode(StandardReadable):
8
36
  def __init__(
@@ -10,8 +38,13 @@ class Diode(StandardReadable):
10
38
  prefix: str,
11
39
  name: str = "",
12
40
  ):
13
- self._prefix = prefix
14
41
  with self.add_children_as_readables():
15
42
  self.signal = epics_signal_r(float, prefix + "B:DIODE:I")
43
+ self.positioner_a = create_positioner(
44
+ FilterAValues, prefix + "A:MP", positioner_pv_suffix=":SELECT"
45
+ ) # more complex, will be fixed on Tuesday 20.05.2025
46
+ self.positioner_b = create_positioner(
47
+ FilterBValues, prefix + "B:MP", positioner_pv_suffix=":SELECT"
48
+ )
16
49
 
17
50
  super().__init__(name=name)
@@ -38,7 +38,7 @@ class MetadataHolder:
38
38
  if isinstance(value, tuple):
39
39
  return {"units": value[1], **datakey(value[0])}
40
40
  dtype = "string"
41
- shape = []
41
+ shape: list[int | None] = []
42
42
  match value:
43
43
  case bool():
44
44
  dtype = "boolean"
@@ -1,6 +1,11 @@
1
+ import asyncio
1
2
  from math import isclose
2
3
 
3
- from ophyd_async.core import StandardReadable, StrictEnum, derived_signal_r
4
+ from ophyd_async.core import (
5
+ StandardReadable,
6
+ StrictEnum,
7
+ derived_signal_rw,
8
+ )
4
9
  from ophyd_async.epics.motor import Motor
5
10
 
6
11
  from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
@@ -37,8 +42,7 @@ class Beamstop(StandardReadable):
37
42
  x: beamstop x position in mm
38
43
  y: beamstop y position in mm
39
44
  z: beamstop z position in mm
40
- selected_pos: Get the current position of the beamstop as an enum. Currently this
41
- is read-only.
45
+ selected_pos: Get or set the current position of the beamstop as an enum.
42
46
  """
43
47
 
44
48
  def __init__(
@@ -51,10 +55,13 @@ class Beamstop(StandardReadable):
51
55
  self.x_mm = Motor(prefix + "X")
52
56
  self.y_mm = Motor(prefix + "Y")
53
57
  self.z_mm = Motor(prefix + "Z")
54
- self.selected_pos = derived_signal_r(
55
- self._get_selected_position, x=self.x_mm, y=self.y_mm, z=self.z_mm
58
+ self.selected_pos = derived_signal_rw(
59
+ self._get_selected_position,
60
+ self._set_selected_position,
61
+ x=self.x_mm,
62
+ y=self.y_mm,
63
+ z=self.z_mm,
56
64
  )
57
-
58
65
  self._in_beam_xyz_mm = [
59
66
  float(beamline_parameters[f"in_beam_{axis}_STANDARD"])
60
67
  for axis in ("x", "y", "z")
@@ -77,3 +84,13 @@ class Beamstop(StandardReadable):
77
84
  return BeamstopPositions.DATA_COLLECTION
78
85
  else:
79
86
  return BeamstopPositions.UNKNOWN
87
+
88
+ async def _set_selected_position(self, position: BeamstopPositions) -> None:
89
+ if position == BeamstopPositions.DATA_COLLECTION:
90
+ await asyncio.gather(
91
+ self.x_mm.set(self._in_beam_xyz_mm[0]),
92
+ self.y_mm.set(self._in_beam_xyz_mm[1]),
93
+ self.z_mm.set(self._in_beam_xyz_mm[2]),
94
+ )
95
+ elif position == BeamstopPositions.UNKNOWN:
96
+ raise ValueError(f"Cannot set beamstop to position {position}")