dls-dodal 1.65.0__py3-none-any.whl → 1.67.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 (85) hide show
  1. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/METADATA +3 -4
  2. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/RECORD +82 -66
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/aithre.py +21 -2
  5. dodal/beamlines/i03.py +102 -198
  6. dodal/beamlines/i04.py +40 -4
  7. dodal/beamlines/i05.py +28 -1
  8. dodal/beamlines/i06.py +62 -0
  9. dodal/beamlines/i07.py +20 -0
  10. dodal/beamlines/i09_1.py +32 -3
  11. dodal/beamlines/i09_2.py +57 -2
  12. dodal/beamlines/i10_optics.py +46 -17
  13. dodal/beamlines/i17.py +7 -3
  14. dodal/beamlines/i18.py +3 -3
  15. dodal/beamlines/i19_1.py +26 -14
  16. dodal/beamlines/i19_2.py +49 -38
  17. dodal/beamlines/i21.py +2 -2
  18. dodal/beamlines/i22.py +19 -4
  19. dodal/beamlines/p38.py +3 -3
  20. dodal/beamlines/training_rig.py +0 -16
  21. dodal/cli.py +26 -12
  22. dodal/common/coordination.py +3 -2
  23. dodal/device_manager.py +604 -0
  24. dodal/devices/aithre_lasershaping/goniometer.py +26 -9
  25. dodal/devices/aperturescatterguard.py +3 -2
  26. dodal/devices/areadetector/plugins/mjpg.py +10 -3
  27. dodal/devices/beamsize/__init__.py +0 -0
  28. dodal/devices/beamsize/beamsize.py +6 -0
  29. dodal/devices/cryostream.py +28 -57
  30. dodal/devices/detector/det_resolution.py +4 -2
  31. dodal/devices/eiger.py +26 -18
  32. dodal/devices/fast_grid_scan.py +14 -2
  33. dodal/devices/i03/beamsize.py +35 -0
  34. dodal/devices/i03/constants.py +7 -0
  35. dodal/devices/i03/undulator_dcm.py +2 -2
  36. dodal/devices/i04/beamsize.py +45 -0
  37. dodal/devices/i04/max_pixel.py +38 -0
  38. dodal/devices/i04/murko_results.py +36 -26
  39. dodal/devices/i04/transfocator.py +23 -29
  40. dodal/devices/i07/id.py +38 -0
  41. dodal/devices/i09_1_shared/__init__.py +13 -2
  42. dodal/devices/i09_1_shared/hard_energy.py +112 -0
  43. dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
  44. dodal/devices/i09_2_shared/__init__.py +0 -0
  45. dodal/devices/i09_2_shared/i09_apple2.py +86 -0
  46. dodal/devices/i10/i10_apple2.py +39 -331
  47. dodal/devices/i17/i17_apple2.py +37 -22
  48. dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
  49. dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
  50. dodal/devices/i19/access_controlled/shutter.py +2 -4
  51. dodal/devices/insertion_device/__init__.py +0 -0
  52. dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +122 -69
  53. dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
  54. dodal/devices/insertion_device/lookup_table_models.py +287 -0
  55. dodal/devices/ipin.py +20 -2
  56. dodal/devices/motors.py +33 -3
  57. dodal/devices/mx_phase1/beamstop.py +31 -12
  58. dodal/devices/oav/oav_calculations.py +9 -4
  59. dodal/devices/oav/oav_detector.py +65 -7
  60. dodal/devices/oav/oav_parameters.py +3 -1
  61. dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
  62. dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
  63. dodal/devices/oav/pin_image_recognition/utils.py +23 -1
  64. dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
  65. dodal/devices/oav/utils.py +16 -6
  66. dodal/devices/robot.py +33 -18
  67. dodal/devices/scintillator.py +36 -14
  68. dodal/devices/smargon.py +2 -3
  69. dodal/devices/thawer.py +7 -45
  70. dodal/devices/undulator.py +152 -68
  71. dodal/plans/__init__.py +1 -1
  72. dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
  73. dodal/plans/load_panda_yaml.py +9 -0
  74. dodal/plans/verify_undulator_gap.py +2 -2
  75. dodal/testing/fixtures/devices/__init__.py +0 -0
  76. dodal/testing/fixtures/devices/apple2.py +78 -0
  77. dodal/utils.py +6 -3
  78. dodal/beamline_specific_utils/i03.py +0 -17
  79. dodal/testing/__init__.py +0 -3
  80. dodal/testing/setup.py +0 -67
  81. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/WHEEL +0 -0
  82. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/entry_points.txt +0 -0
  83. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/licenses/LICENSE +0 -0
  84. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/top_level.txt +0 -0
  85. /dodal/plans/{scanspec.py → spec_path.py} +0 -0
@@ -1,12 +1,7 @@
1
- import csv
2
- import io
3
- from dataclasses import dataclass
4
- from pathlib import Path
5
- from typing import Any, SupportsFloat
1
+ from typing import SupportsFloat
6
2
 
7
3
  import numpy as np
8
4
  from bluesky.protocols import Movable
9
- from daq_config_server.client import ConfigServer
10
5
  from ophyd_async.core import (
11
6
  AsyncStatus,
12
7
  Reference,
@@ -15,308 +10,30 @@ from ophyd_async.core import (
15
10
  derived_signal_rw,
16
11
  soft_signal_rw,
17
12
  )
18
- from pydantic import BaseModel, ConfigDict, RootModel
19
13
 
20
- from dodal.devices.apple2_undulator import (
14
+ from dodal.devices.insertion_device.apple2_undulator import (
15
+ MAXIMUM_MOVE_TIME,
21
16
  Apple2,
22
17
  Apple2Controller,
18
+ Apple2PhasesVal,
23
19
  Apple2Val,
24
20
  Pol,
25
21
  UndulatorGap,
26
22
  UndulatorJawPhase,
27
23
  UndulatorPhaseAxes,
28
24
  )
29
- from dodal.log import LOGGER
25
+ from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
30
26
 
31
27
  ROW_PHASE_MOTOR_TOLERANCE = 0.004
32
28
  MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
33
29
  MAXIMUM_GAP_MOTOR_POSITION = 100
34
30
  DEFAULT_JAW_PHASE_POLY_PARAMS = [1.0 / 7.5, -120.0 / 7.5]
35
31
  ALPHA_OFFSET = 180
36
- MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
37
32
 
38
33
 
39
- # data class to store the lookup table configuration that is use in convert_csv_to_lookup
40
- @dataclass
41
- class LookupPath:
42
- Gap: Path
43
- Phase: Path
34
+ class I10Apple2(Apple2[UndulatorPhaseAxes]):
35
+ """I10Apple2 device is an apple2 with extra jaw phase motor."""
44
36
 
45
-
46
- @dataclass
47
- class LookupTableConfig:
48
- path: LookupPath
49
- source: tuple[str, str]
50
- mode: str | None
51
- min_energy: str | None
52
- max_energy: str | None
53
- poly_deg: list | None
54
-
55
-
56
- class EnergyMinMax(BaseModel):
57
- Minimum: float
58
- Maximum: float
59
-
60
-
61
- class EnergyCoverageEntry(BaseModel):
62
- model_config = ConfigDict(arbitrary_types_allowed=True)
63
- Low: float
64
- High: float
65
- Poly: np.poly1d
66
-
67
-
68
- class EnergyCoverage(RootModel):
69
- root: dict[str, EnergyCoverageEntry]
70
-
71
-
72
- class LookupTableEntries(BaseModel):
73
- Energies: EnergyCoverage
74
- Limit: EnergyMinMax
75
-
76
-
77
- class Lookuptable(RootModel):
78
- """BaseModel class for the lookup table.
79
- Apple2 lookup table should be in this format.
80
-
81
- {mode: {'Energies': {Any: {'Low': float,
82
- 'High': float,
83
- 'Poly':np.poly1d
84
- }
85
- }
86
- 'Limit': {'Minimum': float,
87
- 'Maximum': float
88
- }
89
- }
90
- }
91
- """
92
-
93
- root: dict[str, LookupTableEntries]
94
-
95
-
96
- class I10EnergyMotorLookup:
97
- """
98
- Handles lookup tables for I10 Apple2 ID, converting energy and polarisation to gap
99
- and phase. Fetches and parses lookup tables from a config server, supports dynamic
100
- updates, and validates input.
101
- """
102
-
103
- def __init__(
104
- self,
105
- lookuptable_dir: str,
106
- source: tuple[str, str],
107
- config_client: ConfigServer,
108
- mode: str = "Mode",
109
- min_energy: str = "MinEnergy",
110
- max_energy: str = "MaxEnergy",
111
- gap_file_name: str = "IDEnergy2GapCalibrations.csv",
112
- phase_file_name: str = "IDEnergy2PhaseCalibrations.csv",
113
- poly_deg: list | None = None,
114
- ):
115
- """Initialise the I10EnergyMotorLookup class with lookup table headers provided.
116
-
117
- Parameters
118
- ----------
119
- look_up_table_dir:
120
- The path to look up table.
121
- source:
122
- The column name and the name of the source in look up table. e.g. ( "source", "idu")
123
- config_client:
124
- The config server client to fetch the look up table.
125
- mode:
126
- The column name of the mode in look up table.
127
- min_energy:
128
- The column name that contain the maximum energy in look up table.
129
- max_energy:
130
- The column name that contain the maximum energy in look up table.
131
- poly_deg:
132
- The column names for the parameters for the energy conversion polynomial, starting with the least significant.
133
-
134
- """
135
- self.lookup_tables: dict[str, dict[str | None, dict[str, dict[str, Any]]]] = {
136
- "Gap": {},
137
- "Phase": {},
138
- }
139
- energy_gap_table_path = Path(lookuptable_dir, gap_file_name)
140
- energy_phase_table_path = Path(lookuptable_dir, phase_file_name)
141
- self.lookup_table_config = LookupTableConfig(
142
- path=LookupPath(Gap=energy_gap_table_path, Phase=energy_phase_table_path),
143
- source=source,
144
- mode=mode,
145
- min_energy=min_energy,
146
- max_energy=max_energy,
147
- poly_deg=poly_deg,
148
- )
149
- self.config_client = config_client
150
- self._available_pol = []
151
-
152
- @property
153
- def available_pol(self) -> list[str | None]:
154
- return self._available_pol
155
-
156
- @available_pol.setter
157
- def available_pol(self, value: list[str | None]) -> None:
158
- self._available_pol = value
159
-
160
- def update_lookuptable(self):
161
- """
162
- Update lookup tables from files and validate their format.
163
- """
164
- LOGGER.info("Updating lookup dictionary from file.")
165
- for key, path in self.lookup_table_config.path.__dict__.items():
166
- self.lookup_tables[key] = self.convert_csv_to_lookup(
167
- file=path,
168
- source=self.lookup_table_config.source,
169
- mode=self.lookup_table_config.mode,
170
- min_energy=self.lookup_table_config.min_energy,
171
- max_energy=self.lookup_table_config.max_energy,
172
- poly_deg=self.lookup_table_config.poly_deg,
173
- )
174
- Lookuptable.model_validate(self.lookup_tables[key])
175
-
176
- self.available_pol = list(self.lookup_tables["Gap"].keys())
177
-
178
- def get_motor_from_energy(self, energy: float, pol: Pol) -> tuple[float, float]:
179
- """
180
- Convert energy and polarisation to gap and phase motor positions.
181
-
182
- Parameters
183
- ----------
184
- energy : float
185
- Desired energy in eV.
186
- pol : Pol
187
- Polarisation mode.
188
-
189
- Returns
190
- -------
191
- tuple[float, float]
192
- (gap, phase) motor positions.
193
-
194
- """
195
- if self.available_pol == []:
196
- self.update_lookuptable()
197
-
198
- gap_poly = self._get_poly(
199
- lookup_table=self.lookup_tables["Gap"], energy=energy, pol=pol
200
- )
201
- phase_poly = self._get_poly(
202
- lookup_table=self.lookup_tables["Phase"], energy=energy, pol=pol
203
- )
204
- return gap_poly(energy), phase_poly(energy)
205
-
206
- def _get_poly(
207
- self,
208
- energy: float,
209
- pol: Pol,
210
- lookup_table: dict[str | None, dict[str, dict[str, Any]]],
211
- ) -> np.poly1d:
212
- """
213
- Get polynomial for a given energy and polarisation.
214
-
215
- Raises
216
- ------
217
- ValueError
218
- If energy is out of bounds or coefficients are missing.
219
- """
220
- if (
221
- energy < lookup_table[pol]["Limit"]["Minimum"]
222
- or energy > lookup_table[pol]["Limit"]["Maximum"]
223
- ):
224
- raise ValueError(
225
- "Demanding energy must lie between {} and {} eV!".format(
226
- lookup_table[pol]["Limit"]["Minimum"],
227
- lookup_table[pol]["Limit"]["Maximum"],
228
- )
229
- )
230
- else:
231
- for energy_range in lookup_table[pol]["Energies"].values():
232
- if energy >= energy_range["Low"] and energy < energy_range["High"]:
233
- return energy_range["Poly"]
234
-
235
- raise ValueError(
236
- """Cannot find polynomial coefficients for your requested energy.
237
- There might be gap in the calibration lookup table."""
238
- )
239
-
240
- def convert_csv_to_lookup(
241
- self,
242
- file: str,
243
- source: tuple[str, str],
244
- mode: str | None = "Mode",
245
- min_energy: str | None = "MinEnergy",
246
- max_energy: str | None = "MaxEnergy",
247
- poly_deg: list | None = None,
248
- ) -> dict[str | None, dict[str, dict[str, dict[str, Any]]]]:
249
- """
250
- Convert a CSV file to a lookup table dictionary.
251
-
252
- Returns
253
- -------
254
- dict
255
- Dictionary in Apple2 lookup table format.
256
-
257
- Raises
258
- ------
259
- RuntimeError
260
- If the CSV cannot be converted.
261
-
262
- """
263
- if poly_deg is None:
264
- poly_deg = [
265
- "7th-order",
266
- "6th-order",
267
- "5th-order",
268
- "4th-order",
269
- "3rd-order",
270
- "2nd-order",
271
- "1st-order",
272
- "b",
273
- ]
274
- lookup_table = {}
275
- polarisations = set()
276
-
277
- def process_row(row: dict) -> None:
278
- """Process a single row from the CSV file and update the lookup table."""
279
- mode_value = row[mode]
280
- if mode_value not in polarisations:
281
- polarisations.add(mode_value)
282
- lookup_table[mode_value] = {
283
- "Energies": {},
284
- "Limit": {
285
- "Minimum": float(row[min_energy]),
286
- "Maximum": float(row[max_energy]),
287
- },
288
- }
289
-
290
- # Create polynomial object for energy-to-gap/phase conversion
291
- coefficients = [float(row[coef]) for coef in poly_deg]
292
- polynomial = np.poly1d(coefficients)
293
-
294
- lookup_table[mode_value]["Energies"][row[min_energy]] = {
295
- "Low": float(row[min_energy]),
296
- "High": float(row[max_energy]),
297
- "Poly": polynomial,
298
- }
299
-
300
- # Update energy limits
301
- lookup_table[mode_value]["Limit"]["Minimum"] = min(
302
- lookup_table[mode_value]["Limit"]["Minimum"], float(row[min_energy])
303
- )
304
- lookup_table[mode_value]["Limit"]["Maximum"] = max(
305
- lookup_table[mode_value]["Limit"]["Maximum"], float(row[max_energy])
306
- )
307
-
308
- csv_file = self.config_client.get_file_contents(file, reset_cached_result=True)
309
- reader = csv.DictReader(io.StringIO(csv_file))
310
- for row in reader:
311
- # If there are multiple source only convert requested.
312
- if row[source[0]] == source[1]:
313
- process_row(row=row)
314
- if not lookup_table:
315
- raise RuntimeError(f"Unable to convert lookup table:\t{file}")
316
- return lookup_table
317
-
318
-
319
- class I10Apple2(Apple2):
320
37
  def __init__(
321
38
  self,
322
39
  id_gap: UndulatorGap,
@@ -325,11 +42,8 @@ class I10Apple2(Apple2):
325
42
  name: str = "",
326
43
  ) -> None:
327
44
  """
328
- I10Apple2 device is an apple2 with extra jaw phase motor.
329
-
330
- Parameters
331
- ----------
332
-
45
+ Parameters:
46
+ ------------
333
47
  id_gap : UndulatorJawPhase
334
48
  The gap motor of the undulator.
335
49
  id_phase : UndulatorJawPhase
@@ -347,53 +61,49 @@ class I10Apple2(Apple2):
347
61
  class I10Apple2Controller(Apple2Controller[I10Apple2]):
348
62
  """
349
63
  I10Apple2Controller is a extension of Apple2Controller which provide linear
350
- arbitrary angle control.
64
+ arbitrary angle control.
351
65
  """
352
66
 
353
67
  def __init__(
354
68
  self,
355
69
  apple2: I10Apple2,
356
- lookuptable_dir: str,
357
- source: tuple[str, str],
358
- config_client: ConfigServer,
70
+ gap_energy_motor_lut: EnergyMotorLookup,
71
+ phase_energy_motor_lut: EnergyMotorLookup,
359
72
  jaw_phase_limit: float = 12.0,
360
73
  jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
361
74
  angle_threshold_deg=30.0,
75
+ units: str = "eV",
362
76
  name: str = "",
363
77
  ) -> None:
364
78
  """
365
-
366
- parameters
367
- ----------
79
+ Parameters:
80
+ -----------
368
81
  apple2 : I10Apple2
369
82
  An I10Apple2 device.
370
- lookuptable_dir : str
371
- The path to look up table.
372
- source : tuple[str, str]
373
- The column name and the name of the source in look up table. e.g. ( "source", "idu")
374
- config_client : ConfigServer
375
- The config server client to fetch the look up table.
83
+ gap_energy_motor_lut: EnergyMotorLookup
84
+ The class that handles the gap look up table logic for the insertion device.
85
+ phase_energy_motor_lut: EnergyMotorLookup
86
+ The class that handles the phase look up table logic for the insertion device.
376
87
  jaw_phase_limit : float, optional
377
88
  The maximum allowed jaw_phase movement., by default 12.0
378
89
  jaw_phase_poly_param : list[float], optional
379
90
  polynomial parameters highest power first., by default DEFAULT_JAW_PHASE_POLY_PARAMS
380
91
  angle_threshold_deg : float, optional
381
92
  The angle threshold to switch between 0-180 and 180-360 range., by default 30.0
93
+ units:
94
+ the units of this device. Defaults to eV.
382
95
  name : str, optional
383
96
  New device name.
384
97
  """
385
-
386
- self.lookup_table_client = I10EnergyMotorLookup(
387
- lookuptable_dir=lookuptable_dir,
388
- source=source,
389
- config_client=config_client,
390
- )
98
+ self.gap_energy_motor_lut = gap_energy_motor_lut
99
+ self.phase_energy_motor_lut = phase_energy_motor_lut
391
100
  super().__init__(
392
101
  apple2=apple2,
393
- energy_to_motor_converter=self.lookup_table_client.get_motor_from_energy,
102
+ gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
103
+ phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
104
+ units=units,
394
105
  name=name,
395
106
  )
396
-
397
107
  self.jaw_phase_from_angle = np.poly1d(jaw_phase_poly_param)
398
108
  self.angle_threshold_deg = angle_threshold_deg
399
109
  self.jaw_phase_limit = jaw_phase_limit
@@ -428,24 +138,22 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
428
138
  await self.apple2().jaw_phase().set(jaw_phase)
429
139
  await self._linear_arbitrary_angle.set(pol_angle)
430
140
 
431
- async def _set_motors_from_energy(self, value: float) -> None:
432
- """
433
- Set the undulator motors for a given energy and polarisation.
434
- """
435
-
436
- pol = await self._check_and_get_pol_setpoint()
437
- gap, phase = self.energy_to_motor(energy=value, pol=pol)
141
+ def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
438
142
  phase3 = phase * (-1 if pol == Pol.LA else 1)
439
- id_set_val = Apple2Val(
440
- top_outer=f"{phase:.6f}",
441
- top_inner="0.0",
442
- btm_inner=f"{phase3:.6f}",
443
- btm_outer="0.0",
143
+ return Apple2Val(
444
144
  gap=f"{gap:.6f}",
145
+ phase=Apple2PhasesVal(
146
+ top_outer=f"{phase:.6f}",
147
+ top_inner="0.0",
148
+ btm_inner=f"{phase3:.6f}",
149
+ btm_outer="0.0",
150
+ ),
445
151
  )
446
152
 
447
- LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
448
- await self.apple2().set(id_motor_values=id_set_val)
153
+ async def _set_motors_from_energy_and_polarisation(
154
+ self, energy: float, pol: Pol
155
+ ) -> None:
156
+ await super()._set_motors_from_energy_and_polarisation(energy, pol)
449
157
  if pol != Pol.LA:
450
158
  await self.apple2().jaw_phase().set(0)
451
159
  await self.apple2().jaw_phase().set_move.set(1)
@@ -486,4 +194,4 @@ class LinearArbitraryAngle(StandardReadable, Movable[SupportsFloat]):
486
194
 
487
195
  @AsyncStatus.wrap
488
196
  async def set(self, angle: float) -> None:
489
- await self.linear_arbitrary_angle().set(angle)
197
+ await self.linear_arbitrary_angle().set(angle, timeout=MAXIMUM_MOVE_TIME)
@@ -1,10 +1,12 @@
1
- from dodal.devices.apple2_undulator import (
1
+ from dodal.devices.insertion_device.apple2_undulator import (
2
2
  Apple2,
3
3
  Apple2Controller,
4
+ Apple2PhasesVal,
4
5
  Apple2Val,
5
- EnergyMotorConvertor,
6
+ Pol,
7
+ UndulatorPhaseAxes,
6
8
  )
7
- from dodal.log import LOGGER
9
+ from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
8
10
 
9
11
  ROW_PHASE_MOTOR_TOLERANCE = 0.004
10
12
  MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
@@ -14,7 +16,7 @@ ALPHA_OFFSET = 180
14
16
  MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
15
17
 
16
18
 
17
- class I17Apple2Controller(Apple2Controller[Apple2]):
19
+ class I17Apple2Controller(Apple2Controller[Apple2[UndulatorPhaseAxes]]):
18
20
  """
19
21
  I10Apple2Controller is a extension of Apple2Controller which provide linear
20
22
  arbitrary angle control.
@@ -22,30 +24,43 @@ class I17Apple2Controller(Apple2Controller[Apple2]):
22
24
 
23
25
  def __init__(
24
26
  self,
25
- apple2: Apple2,
26
- energy_to_motor_converter: EnergyMotorConvertor,
27
+ apple2: Apple2[UndulatorPhaseAxes],
28
+ gap_energy_motor_lut: EnergyMotorLookup,
29
+ phase_energy_motor_lut: EnergyMotorLookup,
30
+ units: str = "eV",
27
31
  name: str = "",
28
32
  ) -> None:
33
+ """
34
+ Parameters:
35
+ -----------
36
+ apple2 : Apple2
37
+ An Apple2 device.
38
+ gap_energy_motor_lut: EnergyMotorLookup
39
+ The class that handles the gap look up table logic for the insertion device.
40
+ phase_energy_motor_lut: EnergyMotorLookup
41
+ The class that handles the phase look up table logic for the insertion device.
42
+ units:
43
+ the units of this device. Defaults to eV.
44
+ name : str, optional
45
+ New device name.
46
+ """
47
+ self.gap_energy_motor_lut = gap_energy_motor_lut
48
+ self.phase_energy_motor_lut = phase_energy_motor_lut
29
49
  super().__init__(
30
50
  apple2=apple2,
31
- energy_to_motor_converter=energy_to_motor_converter,
51
+ gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
52
+ phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
53
+ units=units,
32
54
  name=name,
33
55
  )
34
56
 
35
- async def _set_motors_from_energy(self, value: float) -> None:
36
- """
37
- Set the undulator motors for a given energy and polarisation.
38
- """
39
-
40
- pol = await self._check_and_get_pol_setpoint()
41
- gap, phase = self.energy_to_motor(energy=value, pol=pol)
42
- id_set_val = Apple2Val(
43
- top_outer=f"{phase:.6f}",
44
- top_inner="0.0",
45
- btm_inner=f"{phase:.6f}",
46
- btm_outer="0.0",
57
+ def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
58
+ return Apple2Val(
47
59
  gap=f"{gap:.6f}",
60
+ phase=Apple2PhasesVal(
61
+ top_outer=f"{phase:.6f}",
62
+ top_inner=f"{0.0:.6f}",
63
+ btm_inner=f"{phase:.6f}",
64
+ btm_outer=f"{0.0:.6f}",
65
+ ),
48
66
  )
49
-
50
- LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
51
- await self.apple2().set(id_motor_values=id_set_val)
@@ -0,0 +1,61 @@
1
+ from typing import Annotated, Any, Self
2
+
3
+ from ophyd_async.core import AsyncStatus
4
+ from pydantic import BaseModel, model_validator
5
+ from pydantic.types import PositiveInt, StringConstraints
6
+
7
+ from dodal.devices.i19.access_controlled.blueapi_device import (
8
+ OpticsBlueAPIDevice,
9
+ )
10
+ from dodal.devices.i19.access_controlled.hutch_access import ACCESS_DEVICE_NAME
11
+
12
+ PermittedKeyStr = Annotated[str, StringConstraints(pattern="^[A-Za-z0-9-_]*$")]
13
+
14
+
15
+ class AttenuatorMotorPositionDemands(BaseModel):
16
+ continuous_demands: dict[PermittedKeyStr, float] = {}
17
+ indexed_demands: dict[PermittedKeyStr, PositiveInt] = {}
18
+
19
+ @model_validator(mode="after")
20
+ def no_keys_clash(self) -> Self:
21
+ common_keys = set(self.continuous_demands).intersection(self.indexed_demands)
22
+ common_key_count = sum(1 for _ in common_keys)
23
+ if common_key_count < 1:
24
+ return self
25
+ else:
26
+ ks: str = "key" if common_key_count == 1 else "keys"
27
+ error_msg = f"Common {ks} found in distinct motor demands: {common_keys}"
28
+ raise ValueError(error_msg)
29
+
30
+ def validated_complete_demand(self) -> dict[PermittedKeyStr, Any]:
31
+ return self.continuous_demands | self.indexed_demands
32
+
33
+
34
+ class AttenuatorMotorSquad(OpticsBlueAPIDevice):
35
+ """ I19-specific proxy device which requests absorber position changes in the x-ray attenuator.
36
+
37
+ Sends REST call to blueapi controlling optics on the I19 cluster.
38
+ The hutch in use is compared against the hutch which sent the REST call.
39
+ Only the hutch in use will be permitted to execute a plan (requesting motor moves).
40
+ As the two hutches are located in series, checking the hutch in use is necessary to \
41
+ avoid accidentally operating optics devices from one hutch while the other has beam time.
42
+
43
+ The name of the hutch that wants to operate the optics device is passed to the \
44
+ access controlled device upon instantiation of the latter.
45
+
46
+ For details see the architecture described in \
47
+ https://github.com/DiamondLightSource/i19-bluesky/issues/30.
48
+ """
49
+
50
+ @AsyncStatus.wrap
51
+ async def set(self, value: AttenuatorMotorPositionDemands):
52
+ request_params = {
53
+ "name": "operate_motor_squad_plan",
54
+ "params": {
55
+ "experiment_hutch": self._invoking_hutch,
56
+ "access_device": ACCESS_DEVICE_NAME,
57
+ "attenuator_demands": value.validated_complete_demand(),
58
+ },
59
+ "instrument_session": self.instrument_session,
60
+ }
61
+ await super().set(request_params)
@@ -29,11 +29,19 @@ class OpticsBlueAPIDevice(StandardReadable, Movable[D]):
29
29
  https://github.com/DiamondLightSource/i19-bluesky/issues/30.
30
30
  """
31
31
 
32
- def __init__(self, name: str = "") -> None:
32
+ def __init__(
33
+ self, hutch: HutchState, instrument_session: str = "", name: str = ""
34
+ ) -> None:
35
+ self.hutch_request = hutch
36
+ self.instrument_session = instrument_session
33
37
  self.url = OPTICS_BLUEAPI_URL
34
38
  self.headers = HEADERS
35
39
  super().__init__(name)
36
40
 
41
+ @property
42
+ def _invoking_hutch(self) -> str:
43
+ return self.hutch_request.value
44
+
37
45
  @AsyncStatus.wrap
38
46
  async def set(self, value: D):
39
47
  """ On set send a POST request to the optics blueapi with the name and \
@@ -36,16 +36,14 @@ class AccessControlledShutter(OpticsBlueAPIDevice):
36
36
  # see https://github.com/DiamondLightSource/blueapi/issues/1187
37
37
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
38
38
  self.shutter_status = epics_signal_r(ShutterState, f"{prefix}STA")
39
- self.hutch_request = hutch
40
- self.instrument_session = instrument_session
41
- super().__init__(name)
39
+ super().__init__(hutch=hutch, instrument_session=instrument_session, name=name)
42
40
 
43
41
  @AsyncStatus.wrap
44
42
  async def set(self, value: ShutterDemand):
45
43
  request_params = {
46
44
  "name": "operate_shutter_plan",
47
45
  "params": {
48
- "experiment_hutch": self.hutch_request.value,
46
+ "experiment_hutch": self._invoking_hutch,
49
47
  "access_device": ACCESS_DEVICE_NAME,
50
48
  "shutter_demand": value.value,
51
49
  },
File without changes