dls-dodal 1.66.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 (48) hide show
  1. {dls_dodal-1.66.0.dist-info → dls_dodal-1.67.0.dist-info}/METADATA +1 -1
  2. {dls_dodal-1.66.0.dist-info → dls_dodal-1.67.0.dist-info}/RECORD +47 -37
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/i03.py +92 -208
  5. dodal/beamlines/i04.py +22 -1
  6. dodal/beamlines/i05.py +1 -1
  7. dodal/beamlines/i06.py +1 -1
  8. dodal/beamlines/i09_1.py +26 -2
  9. dodal/beamlines/i09_2.py +57 -2
  10. dodal/beamlines/i10_optics.py +44 -25
  11. dodal/beamlines/i17.py +7 -3
  12. dodal/beamlines/i19_1.py +26 -14
  13. dodal/beamlines/i19_2.py +49 -38
  14. dodal/beamlines/i21.py +2 -2
  15. dodal/beamlines/i22.py +16 -1
  16. dodal/beamlines/training_rig.py +0 -16
  17. dodal/cli.py +26 -12
  18. dodal/common/coordination.py +3 -2
  19. dodal/device_manager.py +604 -0
  20. dodal/devices/cryostream.py +28 -57
  21. dodal/devices/eiger.py +26 -18
  22. dodal/devices/i04/max_pixel.py +38 -0
  23. dodal/devices/i09_1_shared/__init__.py +8 -1
  24. dodal/devices/i09_1_shared/hard_energy.py +112 -0
  25. dodal/devices/i09_2_shared/__init__.py +0 -0
  26. dodal/devices/i09_2_shared/i09_apple2.py +86 -0
  27. dodal/devices/i10/i10_apple2.py +23 -21
  28. dodal/devices/i17/i17_apple2.py +32 -20
  29. dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
  30. dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
  31. dodal/devices/i19/access_controlled/shutter.py +2 -4
  32. dodal/devices/insertion_device/__init__.py +0 -0
  33. dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +36 -28
  34. dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
  35. dodal/devices/insertion_device/lookup_table_models.py +287 -0
  36. dodal/devices/motors.py +14 -0
  37. dodal/devices/robot.py +16 -11
  38. dodal/plans/__init__.py +1 -1
  39. dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
  40. dodal/testing/fixtures/devices/__init__.py +0 -0
  41. dodal/testing/fixtures/devices/apple2.py +78 -0
  42. dodal/utils.py +6 -3
  43. dodal/devices/util/lookup_tables_apple2.py +0 -390
  44. {dls_dodal-1.66.0.dist-info → dls_dodal-1.67.0.dist-info}/WHEEL +0 -0
  45. {dls_dodal-1.66.0.dist-info → dls_dodal-1.67.0.dist-info}/entry_points.txt +0 -0
  46. {dls_dodal-1.66.0.dist-info → dls_dodal-1.67.0.dist-info}/licenses/LICENSE +0 -0
  47. {dls_dodal-1.66.0.dist-info → dls_dodal-1.67.0.dist-info}/top_level.txt +0 -0
  48. /dodal/plans/{scanspec.py → spec_path.py} +0 -0
@@ -1,5 +1,4 @@
1
1
  from ophyd_async.core import (
2
- EnabledDisabled,
3
2
  InOut,
4
3
  StandardReadable,
5
4
  StandardReadableFormat,
@@ -12,20 +11,6 @@ from ophyd_async.epics.core import (
12
11
  )
13
12
 
14
13
 
15
- class CryoStream(StandardReadable):
16
- MAX_TEMP_K = 110
17
- MAX_PRESSURE_BAR = 0.1
18
-
19
- def __init__(self, prefix: str, name: str = ""):
20
- self.course = epics_signal_rw(InOut, f"{prefix}-EA-CJET-01:COARSE:CTRL")
21
- self.fine = epics_signal_rw(InOut, f"{prefix}-EA-CJET-01:FINE:CTRL")
22
- self.temperature_k = epics_signal_r(float, f"{prefix}-EA-CSTRM-01:TEMP")
23
- self.back_pressure_bar = epics_signal_r(
24
- float, f"{prefix}-EA-CSTRM-01:BACKPRESS"
25
- )
26
- super().__init__(name)
27
-
28
-
29
14
  class TurboEnum(StrictEnum):
30
15
  OFF = "Off"
31
16
  ON = "On"
@@ -37,59 +22,33 @@ class CryoStreamSelection(StrictEnum):
37
22
  HC1 = "HC1"
38
23
 
39
24
 
40
- class OxfordCryoStreamController(StandardReadable):
25
+ class OxfordCryoStream(StandardReadable):
41
26
  def __init__(self, prefix: str, name: str = ""):
27
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
28
+ self.temp = epics_signal_r(float, f"{prefix}TEMP")
29
+
42
30
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
43
31
  # Any signals that should be read once at the start of the scan
32
+
44
33
  self.turbo = epics_signal_rw(str, f"{prefix}TURBO")
45
34
  self.turbo_mode = epics_signal_rw(TurboEnum, f"{prefix}TURBOMODE")
46
-
47
- self.serial_comms = epics_signal_rw(EnabledDisabled, f"{prefix}DISABLE")
48
35
  self.status = epics_signal_r(str, f"{prefix}STATUS.SEVR")
49
-
50
- with self.add_children_as_readables():
51
- # Any signals that should be read at every point in the scan
52
-
53
- self.purge = epics_signal_x(f"{prefix}PURGE.PROC")
54
- self.hold = epics_signal_x(f"{prefix}HOLD.PROC")
55
- self.start = epics_signal_x(f"{prefix}RESTART.PROC")
56
- self.pause = epics_signal_x(f"{prefix}PAUSE.PROC")
57
- self.resume = epics_signal_x(f"{prefix}RESUME.PROC")
58
- self.end = epics_signal_x(f"{prefix}END.PROC")
59
- self.stop = epics_signal_x(f"{prefix}STOP.PROC")
60
-
61
- self.ramp_rate = epics_signal_rw(float, f"{prefix}RRATE")
62
- self.ramp_temp = epics_signal_rw(float, f"{prefix}RTEMP")
63
- self.ramp = epics_signal_x(f"{prefix}RAMP.PROC")
64
-
65
- self.plat_time = epics_signal_rw(float, f"{prefix}PTIME")
66
- self.plat = epics_signal_x(f"{prefix}PLAT.PROC")
67
-
68
- self.cool_temp = epics_signal_rw(float, f"{prefix}CTEMP")
69
- self.cool = epics_signal_x(f"{prefix}COOL.PROC")
70
-
71
- self.end_rate = epics_signal_rw(float, f"{prefix}ERATE")
72
-
73
- super().__init__(name)
74
-
75
-
76
- class OxfordCryoStreamStatus(StandardReadable):
77
- def __init__(self, prefix: str, name: str = ""):
78
- with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
79
- # Any signals that should be read once at the start of the scan
80
-
81
36
  self.pump_uptime = epics_signal_r(float, f"{prefix}RUNTIME")
82
37
  self.controller_number = epics_signal_r(float, f"{prefix}CTRLNUM")
83
38
  self.software_version = epics_signal_r(float, f"{prefix}VER")
84
39
  self.evap_adjust = epics_signal_r(float, f"{prefix}EVAPADJUST")
85
40
  self.series = epics_signal_r(str, f"{prefix}SERIES")
41
+ self.error = epics_signal_r(float, f"{prefix}ERROR")
42
+ self.mode = epics_signal_r(str, f"{prefix}RUNMODE")
86
43
 
87
44
  with self.add_children_as_readables():
88
45
  # Any signals that should be read at every point in the scan
46
+ self.ramp_rate = epics_signal_rw(float, f"{prefix}RRATE")
47
+ self.ramp_temp = epics_signal_rw(float, f"{prefix}RTEMP")
48
+ self.plat_time = epics_signal_rw(float, f"{prefix}PTIME")
49
+ self.cool_temp = epics_signal_rw(float, f"{prefix}CTEMP")
50
+ self.end_rate = epics_signal_rw(float, f"{prefix}ERATE")
89
51
  self.setpoint = epics_signal_r(float, f"{prefix}SETPOINT")
90
- self.temp = epics_signal_r(float, f"{prefix}TEMP")
91
- self.error = epics_signal_r(float, f"{prefix}ERROR")
92
- self.mode = epics_signal_r(str, f"{prefix}RUNMODE")
93
52
  self.phase = epics_signal_r(str, f"{prefix}PHASE")
94
53
  self.ramp_rate_setpoint = epics_signal_r(float, f"{prefix}RAMPRATE")
95
54
  self.target_temp = epics_signal_r(float, f"{prefix}TARGETTEMP")
@@ -102,14 +61,27 @@ class OxfordCryoStreamStatus(StandardReadable):
102
61
  self.suct_heat = epics_signal_r(float, f"{prefix}SUCTHEAT")
103
62
  self.back_pressure = epics_signal_r(float, f"{prefix}BACKPRESS")
104
63
 
64
+ self.purge = epics_signal_x(f"{prefix}PURGE.PROC")
65
+ self.hold = epics_signal_x(f"{prefix}HOLD.PROC")
66
+ self.start = epics_signal_x(f"{prefix}RESTART.PROC")
67
+ self.pause = epics_signal_x(f"{prefix}PAUSE.PROC")
68
+ self.resume = epics_signal_x(f"{prefix}RESUME.PROC")
69
+ self.end = epics_signal_x(f"{prefix}END.PROC")
70
+ self.stop = epics_signal_x(f"{prefix}STOP.PROC")
71
+ self.plat = epics_signal_x(f"{prefix}PLAT.PROC")
72
+ self.cool = epics_signal_x(f"{prefix}COOL.PROC")
73
+ self.ramp = epics_signal_x(f"{prefix}RAMP.PROC")
74
+
105
75
  super().__init__(name)
106
76
 
107
77
 
108
- class OxfordCryoStream(StandardReadable):
78
+ class OxfordCryoJet(StandardReadable):
79
+ # TODO: https://github.com/DiamondLightSource/dodal/issues/1486
80
+ # This is a placeholder implementation to get it working with I03, the actual cryojet has many more PVs
109
81
  def __init__(self, prefix: str, name=""):
110
82
  with self.add_children_as_readables():
111
- self.controller = OxfordCryoStreamController(prefix=prefix)
112
- self.status = OxfordCryoStreamStatus(prefix=prefix)
83
+ self.coarse = epics_signal_rw(InOut, f"{prefix}COARSE:CTRL")
84
+ self.fine = epics_signal_rw(InOut, f"{prefix}FINE:CTRL")
113
85
 
114
86
  super().__init__(name)
115
87
 
@@ -126,5 +98,4 @@ class CryoStreamGantry(StandardReadable):
126
98
  self.cryostream_selected = epics_signal_r(
127
99
  int, f"{prefix}-MO-STEP-02:GPIO_INP_BITS.B3"
128
100
  )
129
-
130
101
  super().__init__(name)
dodal/devices/eiger.py CHANGED
@@ -64,14 +64,12 @@ class EigerDetector(Device, Stageable):
64
64
  arming_status = Status()
65
65
  arming_status.set_finished()
66
66
 
67
- disarming_status = Status()
68
- disarming_status.set_finished()
69
-
70
67
  def __init__(self, beamline: str = "i03", *args, **kwargs):
71
68
  super().__init__(*args, **kwargs)
72
69
  self.beamline = beamline
73
70
  # using i03 timeouts as default
74
71
  self.timeouts = AVAILABLE_TIMEOUTS.get(beamline, AVAILABLE_TIMEOUTS["i03"])
72
+ self.disarming_status = None
75
73
 
76
74
  @classmethod
77
75
  def with_params(
@@ -106,6 +104,7 @@ class EigerDetector(Device, Stageable):
106
104
  raise Exception("\n".join(errors))
107
105
 
108
106
  def async_stage(self):
107
+ self.disarming_status = None
109
108
  self.odin.nodes.clear_odin_errors()
110
109
  status_ok, error_message = self.odin.wait_for_odin_initialised(
111
110
  self.timeouts.general_status_timeout
@@ -170,22 +169,31 @@ class EigerDetector(Device, Stageable):
170
169
  def stop(self, *args):
171
170
  """Emergency stop the device, mainly used to clean up after error."""
172
171
  LOGGER.info("Eiger stop() called - cleaning up...")
173
- if not self.disarming_status.done:
172
+ if self.disarming_status and not self.disarming_status.done:
174
173
  LOGGER.info("Eiger still disarming, waiting on disarm")
175
174
  self.disarming_status.wait(self.timeouts.arming_timeout)
175
+ elif not self.disarming_status:
176
+ self.disarming_status = Status()
177
+ try:
178
+ self.wait_on_arming_if_started()
179
+ stop_status = self.odin.stop()
180
+ self.odin.file_writer.start_timeout.set(1).wait(
181
+ self.timeouts.general_status_timeout
182
+ )
183
+ self.disarm_detector()
184
+ stop_status &= self.disable_roi_mode()
185
+ LOGGER.info("Waiting on stop status")
186
+ stop_status.wait(self.timeouts.general_status_timeout)
187
+ # See https://github.com/DiamondLightSource/hyperion/issues/1395
188
+ LOGGER.info("Turning off Eiger dev/shm streaming")
189
+ self.odin.fan.dev_shm_enable.set(0).wait(
190
+ self.timeouts.general_status_timeout
191
+ )
192
+ LOGGER.info("Eiger has successfully been stopped")
193
+ finally:
194
+ self.disarming_status.set_finished()
176
195
  else:
177
- self.wait_on_arming_if_started()
178
- stop_status = self.odin.stop()
179
- self.odin.file_writer.start_timeout.set(1).wait(
180
- self.timeouts.general_status_timeout
181
- )
182
- self.disarm_detector()
183
- stop_status &= self.disable_roi_mode()
184
- stop_status.wait(self.timeouts.general_status_timeout)
185
- # See https://github.com/DiamondLightSource/hyperion/issues/1395
186
- LOGGER.info("Turning off Eiger dev/shm streaming")
187
- self.odin.fan.dev_shm_enable.set(0).wait()
188
- LOGGER.info("Eiger has successfully been stopped")
196
+ LOGGER.info("Already disarmed, doing nothing")
189
197
 
190
198
  def disable_roi_mode(self):
191
199
  return self.change_roi_mode(False)
@@ -194,13 +202,13 @@ class EigerDetector(Device, Stageable):
194
202
  return self.change_roi_mode(True)
195
203
 
196
204
  def change_roi_mode(self, enable: bool) -> StatusBase:
205
+ LOGGER.info(f"Changing ROI mode to {enable}")
197
206
  assert self.detector_params is not None
198
207
  detector_dimensions = (
199
208
  self.detector_params.detector_size_constants.roi_size_pixels
200
209
  if enable
201
210
  else self.detector_params.detector_size_constants.det_size_pixels
202
211
  )
203
-
204
212
  status = self.cam.roi_mode.set(
205
213
  1 if enable else 0, timeout=self.timeouts.general_status_timeout
206
214
  )
@@ -216,7 +224,6 @@ class EigerDetector(Device, Stageable):
216
224
  status &= self.odin.file_writer.num_col_chunks.set(
217
225
  detector_dimensions.width, timeout=self.timeouts.general_status_timeout
218
226
  )
219
-
220
227
  return status
221
228
 
222
229
  def set_cam_pvs(self) -> AndStatus:
@@ -296,6 +303,7 @@ class EigerDetector(Device, Stageable):
296
303
  self.detector_params.omega_increment,
297
304
  timeout=self.timeouts.general_status_timeout,
298
305
  )
306
+
299
307
  return status
300
308
 
301
309
  def set_detector_threshold(self, energy: float, tolerance: float = 0.1) -> Status:
@@ -0,0 +1,38 @@
1
+ import cv2
2
+ import numpy as np
3
+ from bluesky.protocols import Triggerable
4
+ from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_r_and_setter
5
+ from ophyd_async.epics.core import (
6
+ epics_signal_r,
7
+ )
8
+
9
+ # kernal size describes how many of the neigbouring pixels are used for the blur,
10
+ # higher kernal size means more of a blur effect
11
+ KERNAL_SIZE = (7, 7)
12
+
13
+
14
+ class MaxPixel(StandardReadable, Triggerable):
15
+ """Gets the max pixel (brightest pixel) from the image after some image processing."""
16
+
17
+ def __init__(self, prefix: str, name: str = "") -> None:
18
+ self.array_data = epics_signal_r(np.ndarray, f"pva://{prefix}PVA:ARRAY")
19
+ self.max_pixel_val, self._max_val_setter = soft_signal_r_and_setter(float)
20
+ super().__init__(name)
21
+
22
+ async def _convert_to_gray_and_blur(self):
23
+ """
24
+ Preprocess the image array data (convert to grayscale and apply a gaussian blur)
25
+ Image is converted to grayscale (using a weighted mean as green contributes more to brightness)
26
+ as we aren't interested in data relating to colour. A blur is then applied to mitigate
27
+ errors due to rogue hot pixels.
28
+ """
29
+ data = await self.array_data.get_value()
30
+ gray_arr = cv2.cvtColor(data, cv2.COLOR_BGR2GRAY)
31
+ return cv2.GaussianBlur(gray_arr, KERNAL_SIZE, 0)
32
+
33
+ @AsyncStatus.wrap
34
+ async def trigger(self):
35
+ arr = await self._convert_to_gray_and_blur()
36
+ max_val = float(np.max(arr)) # np.int64
37
+ assert isinstance(max_val, float)
38
+ self._max_val_setter(max_val)
@@ -1,7 +1,14 @@
1
+ from .hard_energy import HardEnergy, HardInsertionDeviceEnergy
1
2
  from .hard_undulator_functions import (
2
3
  calculate_energy_i09_hu,
3
4
  calculate_gap_i09_hu,
4
5
  get_hu_lut_as_dict,
5
6
  )
6
7
 
7
- __all__ = ["calculate_gap_i09_hu", "get_hu_lut_as_dict", "calculate_energy_i09_hu"]
8
+ __all__ = [
9
+ "calculate_gap_i09_hu",
10
+ "get_hu_lut_as_dict",
11
+ "calculate_energy_i09_hu",
12
+ "HardInsertionDeviceEnergy",
13
+ "HardEnergy",
14
+ ]
@@ -0,0 +1,112 @@
1
+ from asyncio import gather
2
+ from collections.abc import Callable
3
+
4
+ from bluesky.protocols import Locatable, Location, Movable
5
+ from numpy import ndarray
6
+ from ophyd_async.core import (
7
+ AsyncStatus,
8
+ Reference,
9
+ StandardReadable,
10
+ StandardReadableFormat,
11
+ derived_signal_rw,
12
+ soft_signal_rw,
13
+ )
14
+
15
+ from dodal.devices.common_dcm import DoubleCrystalMonochromatorBase
16
+ from dodal.devices.i09_1_shared.hard_undulator_functions import (
17
+ MAX_ENERGY_COLUMN,
18
+ MIN_ENERGY_COLUMN,
19
+ )
20
+ from dodal.devices.undulator import UndulatorInMm, UndulatorOrder
21
+
22
+
23
+ class HardInsertionDeviceEnergy(StandardReadable, Movable[float]):
24
+ """
25
+ Compound device to link hard x-ray undulator gap and order to photon energy.
26
+ Setting the energy adjusts the undulator gap accordingly.
27
+ """
28
+
29
+ def __init__(
30
+ self,
31
+ undulator_order: UndulatorOrder,
32
+ undulator: UndulatorInMm,
33
+ lut: dict[int, ndarray],
34
+ gap_to_energy_func: Callable[..., float],
35
+ energy_to_gap_func: Callable[..., float],
36
+ name: str = "",
37
+ ) -> None:
38
+ self._lut = lut
39
+ self.gap_to_energy_func = gap_to_energy_func
40
+ self.energy_to_gap_func = energy_to_gap_func
41
+ self._undulator_order_ref = Reference(undulator_order)
42
+ self._undulator_ref = Reference(undulator)
43
+
44
+ self.add_readables([undulator_order, undulator.current_gap])
45
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
46
+ self.energy_demand = soft_signal_rw(float)
47
+ self.energy = derived_signal_rw(
48
+ raw_to_derived=self._read_energy,
49
+ set_derived=self._set_energy,
50
+ current_gap=self._undulator_ref().gap_motor.user_readback,
51
+ current_order=self._undulator_order_ref().value,
52
+ derived_units="keV",
53
+ )
54
+ super().__init__(name=name)
55
+
56
+ def _read_energy(self, current_gap: float, current_order: int) -> float:
57
+ return self.gap_to_energy_func(
58
+ gap=current_gap,
59
+ look_up_table=self._lut,
60
+ order=current_order,
61
+ )
62
+
63
+ async def _set_energy(self, energy: float) -> None:
64
+ current_order = await self._undulator_order_ref().value.get_value()
65
+ min_energy, max_energy = self._lut[current_order][
66
+ MIN_ENERGY_COLUMN : MAX_ENERGY_COLUMN + 1
67
+ ]
68
+ if not (min_energy <= energy <= max_energy):
69
+ raise ValueError(
70
+ f"Requested energy {energy} keV is out of range for harmonic {current_order}: "
71
+ f"[{min_energy}, {max_energy}] keV"
72
+ )
73
+
74
+ target_gap = self.energy_to_gap_func(
75
+ photon_energy_kev=energy, look_up_table=self._lut, order=current_order
76
+ )
77
+ await self._undulator_ref().set(target_gap)
78
+
79
+ @AsyncStatus.wrap
80
+ async def set(self, value: float) -> None:
81
+ self.energy_demand.set(value)
82
+ await self.energy.set(value)
83
+
84
+
85
+ class HardEnergy(StandardReadable, Locatable[float]):
86
+ """
87
+ Energy compound device that provides combined change of both DCM energy and undulator gap accordingly.
88
+ """
89
+
90
+ def __init__(
91
+ self,
92
+ dcm: DoubleCrystalMonochromatorBase,
93
+ undulator_energy: HardInsertionDeviceEnergy,
94
+ name: str = "",
95
+ ) -> None:
96
+ self._dcm_ref = Reference(dcm)
97
+ self._undulator_energy_ref = Reference(undulator_energy)
98
+ self.add_readables([undulator_energy, dcm.energy_in_keV])
99
+ super().__init__(name=name)
100
+
101
+ @AsyncStatus.wrap
102
+ async def set(self, value: float) -> None:
103
+ await gather(
104
+ self._dcm_ref().energy_in_keV.set(value),
105
+ self._undulator_energy_ref().set(value),
106
+ )
107
+
108
+ async def locate(self) -> Location[float]:
109
+ return Location(
110
+ setpoint=await self._dcm_ref().energy_in_keV.user_setpoint.get_value(),
111
+ readback=await self._dcm_ref().energy_in_keV.user_readback.get_value(),
112
+ )
File without changes
@@ -0,0 +1,86 @@
1
+ from dodal.devices.insertion_device.apple2_undulator import (
2
+ MAXIMUM_MOVE_TIME,
3
+ Apple2,
4
+ Apple2Controller,
5
+ Apple2PhasesVal,
6
+ Apple2Val,
7
+ Pol,
8
+ UndulatorPhaseAxes,
9
+ )
10
+ from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
11
+
12
+ J09_GAP_POLY_DEG_COLUMNS = [
13
+ "9th-order",
14
+ "8th-order",
15
+ "7th-order",
16
+ "6th-order",
17
+ "5th-order",
18
+ "4th-order",
19
+ "3rd-order",
20
+ "2nd-order",
21
+ "1st-order",
22
+ "0th-order",
23
+ ]
24
+
25
+ J09_PHASE_POLY_DEG_COLUMNS = ["0th-order"]
26
+
27
+
28
+ class J09Apple2Controller(Apple2Controller[Apple2[UndulatorPhaseAxes]]):
29
+ def __init__(
30
+ self,
31
+ apple2: Apple2[UndulatorPhaseAxes],
32
+ gap_energy_motor_lut: EnergyMotorLookup,
33
+ phase_energy_motor_lut: EnergyMotorLookup,
34
+ units: str = "keV",
35
+ name: str = "",
36
+ ) -> None:
37
+ """
38
+ Parameters:
39
+ -----------
40
+ apple2 : Apple2
41
+ An Apple2 device.
42
+ gap_energy_motor_lut: EnergyMotorLookup
43
+ The class that handles the gap look up table logic for the insertion device.
44
+ phase_energy_motor_lut: EnergyMotorLookup
45
+ The class that handles the phase look up table logic for the insertion device.
46
+ units:
47
+ the units of this device. Defaults to eV.
48
+ name : str, optional
49
+ New device name.
50
+ """
51
+ self.gap_energy_motor_lut = gap_energy_motor_lut
52
+ self.phase_energy_motor_lut = phase_energy_motor_lut
53
+ super().__init__(
54
+ apple2=apple2,
55
+ gap_energy_motor_converter=gap_energy_motor_lut.find_value_in_lookup_table,
56
+ phase_energy_motor_converter=phase_energy_motor_lut.find_value_in_lookup_table,
57
+ units=units,
58
+ name=name,
59
+ )
60
+
61
+ def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
62
+ return Apple2Val(
63
+ gap=f"{gap:.6f}",
64
+ phase=Apple2PhasesVal(
65
+ top_outer=f"{phase:.6f}",
66
+ top_inner=f"{0.0:.6f}",
67
+ btm_inner=f"{phase:.6f}",
68
+ btm_outer=f"{0.0:.6f}",
69
+ ),
70
+ )
71
+
72
+ async def _set_pol(
73
+ self,
74
+ value: Pol,
75
+ ) -> None:
76
+ # I09 require all palarisation change to go via LH.
77
+ target_energy = await self.energy.get_value()
78
+ if value is not Pol.LH:
79
+ self._polarisation_setpoint_set(Pol.LH)
80
+ max_lh_energy = self.gap_energy_motor_lut.lut.root[Pol.LH].max_energy
81
+ lh_setpoint = (
82
+ max_lh_energy if target_energy > max_lh_energy else target_energy
83
+ )
84
+ await self.energy.set(lh_setpoint, timeout=MAXIMUM_MOVE_TIME)
85
+ self._polarisation_setpoint_set(value)
86
+ await self.energy.set(target_energy, timeout=MAXIMUM_MOVE_TIME)
@@ -11,7 +11,7 @@ from ophyd_async.core import (
11
11
  soft_signal_rw,
12
12
  )
13
13
 
14
- from dodal.devices.apple2_undulator import (
14
+ from dodal.devices.insertion_device.apple2_undulator import (
15
15
  MAXIMUM_MOVE_TIME,
16
16
  Apple2,
17
17
  Apple2Controller,
@@ -22,8 +22,7 @@ from dodal.devices.apple2_undulator import (
22
22
  UndulatorJawPhase,
23
23
  UndulatorPhaseAxes,
24
24
  )
25
- from dodal.devices.util.lookup_tables_apple2 import EnergyMotorLookup
26
- from dodal.log import LOGGER
25
+ from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
27
26
 
28
27
  ROW_PHASE_MOTOR_TOLERANCE = 0.004
29
28
  MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
@@ -62,16 +61,18 @@ class I10Apple2(Apple2[UndulatorPhaseAxes]):
62
61
  class I10Apple2Controller(Apple2Controller[I10Apple2]):
63
62
  """
64
63
  I10Apple2Controller is a extension of Apple2Controller which provide linear
65
- arbitrary angle control.
64
+ arbitrary angle control.
66
65
  """
67
66
 
68
67
  def __init__(
69
68
  self,
70
69
  apple2: I10Apple2,
71
- energy_motor_lut: EnergyMotorLookup,
70
+ gap_energy_motor_lut: EnergyMotorLookup,
71
+ phase_energy_motor_lut: EnergyMotorLookup,
72
72
  jaw_phase_limit: float = 12.0,
73
73
  jaw_phase_poly_param: list[float] = DEFAULT_JAW_PHASE_POLY_PARAMS,
74
74
  angle_threshold_deg=30.0,
75
+ units: str = "eV",
75
76
  name: str = "",
76
77
  ) -> None:
77
78
  """
@@ -79,25 +80,30 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
79
80
  -----------
80
81
  apple2 : I10Apple2
81
82
  An I10Apple2 device.
82
- energy_motor_lut: EnergyMotorLookup
83
- The class that handles the look up table logic for the insertion device.
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.
84
87
  jaw_phase_limit : float, optional
85
88
  The maximum allowed jaw_phase movement., by default 12.0
86
89
  jaw_phase_poly_param : list[float], optional
87
90
  polynomial parameters highest power first., by default DEFAULT_JAW_PHASE_POLY_PARAMS
88
91
  angle_threshold_deg : float, optional
89
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.
90
95
  name : str, optional
91
96
  New device name.
92
97
  """
93
-
94
- self.energy_motor_lut = energy_motor_lut
98
+ self.gap_energy_motor_lut = gap_energy_motor_lut
99
+ self.phase_energy_motor_lut = phase_energy_motor_lut
95
100
  super().__init__(
96
101
  apple2=apple2,
97
- energy_to_motor_converter=self.energy_motor_lut.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,
98
105
  name=name,
99
106
  )
100
-
101
107
  self.jaw_phase_from_angle = np.poly1d(jaw_phase_poly_param)
102
108
  self.angle_threshold_deg = angle_threshold_deg
103
109
  self.jaw_phase_limit = jaw_phase_limit
@@ -132,15 +138,9 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
132
138
  await self.apple2().jaw_phase().set(jaw_phase)
133
139
  await self._linear_arbitrary_angle.set(pol_angle)
134
140
 
135
- async def _set_motors_from_energy(self, value: float) -> None:
136
- """
137
- Set the undulator motors for a given energy and polarisation.
138
- """
139
-
140
- pol = await self._check_and_get_pol_setpoint()
141
- gap, phase = self.energy_to_motor(energy=value, pol=pol)
141
+ def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
142
142
  phase3 = phase * (-1 if pol == Pol.LA else 1)
143
- id_set_val = Apple2Val(
143
+ return Apple2Val(
144
144
  gap=f"{gap:.6f}",
145
145
  phase=Apple2PhasesVal(
146
146
  top_outer=f"{phase:.6f}",
@@ -150,8 +150,10 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
150
150
  ),
151
151
  )
152
152
 
153
- LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
154
- 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)
155
157
  if pol != Pol.LA:
156
158
  await self.apple2().jaw_phase().set(0)
157
159
  await self.apple2().jaw_phase().set_move.set(1)
@@ -1,11 +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
4
  Apple2PhasesVal,
5
5
  Apple2Val,
6
- EnergyMotorConvertor,
6
+ Pol,
7
+ UndulatorPhaseAxes,
7
8
  )
8
- from dodal.log import LOGGER
9
+ from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
9
10
 
10
11
  ROW_PHASE_MOTOR_TOLERANCE = 0.004
11
12
  MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
@@ -15,7 +16,7 @@ ALPHA_OFFSET = 180
15
16
  MAXIMUM_MOVE_TIME = 550 # There is no useful movements take longer than this.
16
17
 
17
18
 
18
- class I17Apple2Controller(Apple2Controller[Apple2]):
19
+ class I17Apple2Controller(Apple2Controller[Apple2[UndulatorPhaseAxes]]):
19
20
  """
20
21
  I10Apple2Controller is a extension of Apple2Controller which provide linear
21
22
  arbitrary angle control.
@@ -23,32 +24,43 @@ class I17Apple2Controller(Apple2Controller[Apple2]):
23
24
 
24
25
  def __init__(
25
26
  self,
26
- apple2: Apple2,
27
- 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",
28
31
  name: str = "",
29
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
30
49
  super().__init__(
31
50
  apple2=apple2,
32
- 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,
33
54
  name=name,
34
55
  )
35
56
 
36
- async def _set_motors_from_energy(self, value: float) -> None:
37
- """
38
- Set the undulator motors for a given energy and polarisation.
39
- """
40
-
41
- pol = await self._check_and_get_pol_setpoint()
42
- gap, phase = self.energy_to_motor(energy=value, pol=pol)
43
- id_set_val = Apple2Val(
57
+ def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
58
+ return Apple2Val(
44
59
  gap=f"{gap:.6f}",
45
60
  phase=Apple2PhasesVal(
46
61
  top_outer=f"{phase:.6f}",
47
- top_inner="0.0",
62
+ top_inner=f"{0.0:.6f}",
48
63
  btm_inner=f"{phase:.6f}",
49
- btm_outer="0.0",
64
+ btm_outer=f"{0.0:.6f}",
50
65
  ),
51
66
  )
52
-
53
- LOGGER.info(f"Setting polarisation to {pol}, with values: {id_set_val}")
54
- await self.apple2().set(id_motor_values=id_set_val)