dls-dodal 1.67.0__py3-none-any.whl → 1.69.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 (86) hide show
  1. {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/METADATA +2 -32
  2. {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/RECORD +79 -71
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/adsim.py +30 -23
  5. dodal/beamlines/b07.py +1 -1
  6. dodal/beamlines/b07_1.py +1 -1
  7. dodal/beamlines/i02_1.py +14 -42
  8. dodal/beamlines/i02_2.py +5 -11
  9. dodal/beamlines/i03.py +4 -1
  10. dodal/beamlines/i03_supervisor.py +19 -0
  11. dodal/beamlines/i04.py +74 -179
  12. dodal/beamlines/i05.py +9 -1
  13. dodal/beamlines/i06.py +1 -1
  14. dodal/beamlines/i06_1.py +24 -0
  15. dodal/beamlines/i09.py +53 -9
  16. dodal/beamlines/i09_1.py +9 -1
  17. dodal/beamlines/i09_2.py +7 -6
  18. dodal/beamlines/i10_optics.py +1 -1
  19. dodal/beamlines/i16.py +34 -0
  20. dodal/beamlines/i17.py +1 -1
  21. dodal/beamlines/i20_1.py +14 -0
  22. dodal/beamlines/i21.py +71 -4
  23. dodal/beamlines/i23.py +19 -25
  24. dodal/beamlines/i24.py +55 -105
  25. dodal/beamlines/p60.py +12 -2
  26. dodal/common/__init__.py +2 -1
  27. dodal/common/maths.py +80 -0
  28. dodal/devices/eiger.py +44 -23
  29. dodal/devices/electron_analyser/__init__.py +0 -33
  30. dodal/devices/electron_analyser/base/__init__.py +58 -0
  31. dodal/devices/electron_analyser/base/base_controller.py +84 -0
  32. dodal/devices/electron_analyser/base/base_detector.py +214 -0
  33. dodal/devices/electron_analyser/{abstract → base}/base_driver_io.py +23 -42
  34. dodal/devices/electron_analyser/{enums.py → base/base_enums.py} +0 -5
  35. dodal/devices/electron_analyser/{abstract → base}/base_region.py +48 -11
  36. dodal/devices/electron_analyser/{util.py → base/base_util.py} +1 -1
  37. dodal/devices/electron_analyser/{energy_sources.py → base/energy_sources.py} +27 -26
  38. dodal/devices/electron_analyser/specs/__init__.py +4 -4
  39. dodal/devices/electron_analyser/specs/specs_detector.py +47 -0
  40. dodal/devices/electron_analyser/specs/{driver_io.py → specs_driver_io.py} +23 -26
  41. dodal/devices/electron_analyser/specs/{region.py → specs_region.py} +4 -3
  42. dodal/devices/electron_analyser/vgscienta/__init__.py +4 -4
  43. dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +53 -0
  44. dodal/devices/electron_analyser/vgscienta/{driver_io.py → vgscienta_driver_io.py} +25 -31
  45. dodal/devices/electron_analyser/vgscienta/{region.py → vgscienta_region.py} +6 -6
  46. dodal/devices/fast_shutter.py +108 -25
  47. dodal/devices/i04/beam_centre.py +84 -0
  48. dodal/devices/i04/max_pixel.py +4 -17
  49. dodal/devices/i04/murko_results.py +18 -3
  50. dodal/devices/i09_2_shared/i09_apple2.py +0 -72
  51. dodal/devices/i10/i10_apple2.py +7 -7
  52. dodal/devices/i17/i17_apple2.py +6 -6
  53. dodal/devices/i21/__init__.py +3 -1
  54. dodal/devices/i24/commissioning_jungfrau.py +9 -10
  55. dodal/devices/insertion_device/__init__.py +62 -0
  56. dodal/devices/insertion_device/apple2_controller.py +380 -0
  57. dodal/devices/insertion_device/apple2_undulator.py +152 -481
  58. dodal/devices/insertion_device/energy.py +88 -0
  59. dodal/devices/insertion_device/energy_motor_lookup.py +1 -1
  60. dodal/devices/insertion_device/enum.py +17 -0
  61. dodal/devices/insertion_device/lookup_table_models.py +66 -36
  62. dodal/devices/insertion_device/polarisation.py +36 -0
  63. dodal/devices/oav/oav_detector.py +66 -1
  64. dodal/devices/oav/utils.py +17 -0
  65. dodal/devices/robot.py +35 -18
  66. dodal/devices/selectable_source.py +38 -0
  67. dodal/devices/zebra/zebra.py +15 -0
  68. dodal/devices/zebra/zebra_constants_mapping.py +1 -0
  69. dodal/plans/configure_arm_trigger_and_disarm_detector.py +0 -1
  70. dodal/testing/__init__.py +0 -0
  71. dodal/testing/electron_analyser/device_factory.py +4 -4
  72. dodal/testing/fixtures/devices/apple2.py +1 -1
  73. dodal/testing/fixtures/run_engine.py +4 -0
  74. dodal/devices/electron_analyser/abstract/__init__.py +0 -25
  75. dodal/devices/electron_analyser/abstract/base_detector.py +0 -63
  76. dodal/devices/electron_analyser/abstract/types.py +0 -12
  77. dodal/devices/electron_analyser/detector.py +0 -143
  78. dodal/devices/electron_analyser/specs/detector.py +0 -34
  79. dodal/devices/electron_analyser/types.py +0 -57
  80. dodal/devices/electron_analyser/vgscienta/detector.py +0 -48
  81. {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/WHEEL +0 -0
  82. {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/entry_points.txt +0 -0
  83. {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/licenses/LICENSE +0 -0
  84. {dls_dodal-1.67.0.dist-info → dls_dodal-1.69.0.dist-info}/top_level.txt +0 -0
  85. /dodal/devices/electron_analyser/specs/{enums.py → specs_enums.py} +0 -0
  86. /dodal/devices/electron_analyser/vgscienta/{enums.py → vgscienta_enums.py} +0 -0
@@ -1,22 +1,22 @@
1
1
  import uuid
2
- from typing import Generic
2
+ from typing import Generic, TypeVar
3
3
 
4
+ from ophyd_async.core import StrictEnum
4
5
  from pydantic import Field, field_validator
5
6
 
6
- from dodal.devices.electron_analyser.abstract.base_region import (
7
+ from dodal.devices.electron_analyser.base.base_region import (
7
8
  AbstractBaseRegion,
8
9
  AbstractBaseSequence,
9
- )
10
- from dodal.devices.electron_analyser.abstract.types import (
11
10
  TLensMode,
12
- TPassEnergyEnum,
13
11
  TPsuMode,
14
12
  )
15
- from dodal.devices.electron_analyser.vgscienta.enums import (
13
+ from dodal.devices.electron_analyser.vgscienta.vgscienta_enums import (
16
14
  AcquisitionMode,
17
15
  DetectorMode,
18
16
  )
19
17
 
18
+ TPassEnergyEnum = TypeVar("TPassEnergyEnum", bound=StrictEnum)
19
+
20
20
 
21
21
  class VGScientaRegion(
22
22
  AbstractBaseRegion[AcquisitionMode, TLensMode, TPassEnergyEnum],
@@ -1,19 +1,26 @@
1
- from typing import TypeVar
1
+ from typing import Generic, Protocol, TypeVar
2
2
 
3
3
  from bluesky.protocols import Movable
4
4
  from ophyd_async.core import (
5
5
  AsyncStatus,
6
6
  EnumTypes,
7
+ Reference,
8
+ SignalRW,
7
9
  StandardReadable,
10
+ StandardReadableFormat,
11
+ derived_signal_rw,
12
+ soft_signal_r_and_setter,
8
13
  )
9
14
  from ophyd_async.epics.core import epics_signal_rw
10
15
 
11
- StrictEnumT = TypeVar("StrictEnumT", bound=EnumTypes)
16
+ from dodal.devices.selectable_source import SelectedSource, get_obj_from_selected_source
12
17
 
18
+ EnumTypesT = TypeVar("EnumTypesT", bound=EnumTypes)
13
19
 
14
- class GenericFastShutter(StandardReadable, Movable[StrictEnumT]):
20
+
21
+ class FastShutter(Movable[EnumTypesT], Protocol, Generic[EnumTypesT]):
15
22
  """
16
- Basic enum device specialised for a fast shutter with configured open_state and
23
+ Enum device specialised for a fast shutter with configured open_state and
17
24
  close_state so it is generic enough to be used with any device or plan without
18
25
  knowing the specific enum to use.
19
26
 
@@ -25,11 +32,28 @@ class GenericFastShutter(StandardReadable, Movable[StrictEnumT]):
25
32
  run_engine(bps.mv(shutter, shutter.close_state))
26
33
  """
27
34
 
35
+ open_state: EnumTypesT
36
+ close_state: EnumTypesT
37
+ shutter_state: SignalRW[EnumTypesT]
38
+
39
+ @AsyncStatus.wrap
40
+ async def set(self, state: EnumTypesT):
41
+ await self.shutter_state.set(state)
42
+
43
+
44
+ class GenericFastShutter(
45
+ StandardReadable, FastShutter[EnumTypesT], Generic[EnumTypesT]
46
+ ):
47
+ """
48
+ Implementation of fast shutter that connects to an epics pv. This pv is an enum that
49
+ controls the open and close state of the shutter.
50
+ """
51
+
28
52
  def __init__(
29
53
  self,
30
54
  pv: str,
31
- open_state: StrictEnumT,
32
- close_state: StrictEnumT,
55
+ open_state: EnumTypesT,
56
+ close_state: EnumTypesT,
33
57
  name: str = "",
34
58
  ):
35
59
  """
@@ -41,29 +65,88 @@ class GenericFastShutter(StandardReadable, Movable[StrictEnumT]):
41
65
  self.open_state = open_state
42
66
  self.close_state = close_state
43
67
  with self.add_children_as_readables():
44
- self.state = epics_signal_rw(type(self.open_state), pv)
68
+ self.shutter_state = epics_signal_rw(type(self.open_state), pv)
45
69
  super().__init__(name)
46
70
 
47
- @AsyncStatus.wrap
48
- async def set(self, value: StrictEnumT) -> None:
49
- await self.state.set(value)
50
71
 
51
- async def is_open(self) -> bool:
52
- """
53
- Checks to see if shutter is in open_state. Should not be used directly inside a
54
- plan. A user should use the following instead in a plan:
72
+ class DualFastShutter(StandardReadable, FastShutter[EnumTypesT], Generic[EnumTypesT]):
73
+ """
74
+ A fast shutter device that handles the positions of two other fast shutters. The
75
+ "active" shutter is the one that corrosponds to the selected_shutter signal. For
76
+ example, active shutter is shutter1 if selected_source is at SelectedSource.SOURCE1
77
+ and vise versa for shutter2 and SelectedSource.SOURCE2. Whenever a move is done on
78
+ this device, the inactive shutter is always set to the close_state.
79
+ """
55
80
 
56
- from bluesky import plan_stubs as bps
57
- is_open = yield from bps.rd(shutter.state) == shutter.open_state
81
+ def __init__(
82
+ self,
83
+ shutter1: GenericFastShutter[EnumTypesT],
84
+ shutter2: GenericFastShutter[EnumTypesT],
85
+ selected_source: SignalRW[SelectedSource],
86
+ name: str = "",
87
+ ):
58
88
  """
59
- return await self.state.get_value() == self.open_state
60
-
61
- async def is_closed(self) -> bool:
89
+ Arguments:
90
+ shutter1: Active shutter that corrosponds to SelectedSource.SOURCE1.
91
+ shutter2: Active shutter that corrosponds to SelectedSource.SOURCE2.
92
+ selected_source: Signal that decides the active shutter.
93
+ name: Name of this device.
62
94
  """
63
- Checks to see if shutter is in close_state. Should not be used directly inside a
64
- plan. A user should use the following instead in a plan:
95
+ self._validate_shutter_states(shutter1.open_state, shutter2.open_state)
96
+ self._validate_shutter_states(shutter1.close_state, shutter2.close_state)
97
+ self.open_state = shutter1.open_state
98
+ self.close_state = shutter1.close_state
65
99
 
66
- from bluesky import plan_stubs as bps
67
- is_closed = yield from bps.rd(shutter.state) == shutter.close_state
68
- """
69
- return await self.state.get_value() == self.close_state
100
+ self._shutter1_ref = Reference(shutter1)
101
+ self._shutter2_ref = Reference(shutter2)
102
+ self._selected_shutter_ref = Reference(selected_source)
103
+
104
+ with self.add_children_as_readables():
105
+ self.shutter_state = derived_signal_rw(
106
+ self._read_shutter_state,
107
+ self._set_shutter_state,
108
+ selected_shutter=selected_source,
109
+ shutter1=shutter1.shutter_state,
110
+ shutter2=shutter2.shutter_state,
111
+ )
112
+
113
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
114
+ self.shutter1_device_name, _ = soft_signal_r_and_setter(
115
+ str, initial_value=shutter1.name
116
+ )
117
+ self.shutter2_device_name, _ = soft_signal_r_and_setter(
118
+ str, initial_value=shutter2.name
119
+ )
120
+
121
+ self.add_readables([shutter1, shutter2, selected_source])
122
+
123
+ super().__init__(name)
124
+
125
+ def _validate_shutter_states(self, state1: EnumTypesT, state2: EnumTypesT) -> None:
126
+ if state1 is not state2:
127
+ raise ValueError(
128
+ f"{state1} is not same value as {state2}. They must be the same to be compatible."
129
+ )
130
+
131
+ def _read_shutter_state(
132
+ self,
133
+ selected_shutter: SelectedSource,
134
+ shutter1: EnumTypesT,
135
+ shutter2: EnumTypesT,
136
+ ) -> EnumTypesT:
137
+ return get_obj_from_selected_source(selected_shutter, shutter1, shutter2)
138
+
139
+ async def _set_shutter_state(self, value: EnumTypesT):
140
+ selected_shutter = await self._selected_shutter_ref().get_value()
141
+ active_shutter = get_obj_from_selected_source(
142
+ selected_shutter,
143
+ self._shutter1_ref(),
144
+ self._shutter2_ref(),
145
+ )
146
+ inactive_shutter = get_obj_from_selected_source(
147
+ selected_shutter,
148
+ self._shutter2_ref(),
149
+ self._shutter1_ref(),
150
+ )
151
+ await inactive_shutter.set(inactive_shutter.close_state)
152
+ await active_shutter.set(value)
@@ -0,0 +1,84 @@
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
+ from dodal.devices.oav.utils import convert_to_gray_and_blur
10
+ from dodal.log import LOGGER
11
+
12
+ # Constant was chosen from trial and error with test images
13
+ ADDITIONAL_BINARY_THRESH = 20
14
+
15
+
16
+ def convert_image_to_binary(image: np.ndarray):
17
+ """
18
+ Creates a binary image from OAV image array data.
19
+
20
+ Pixels of the input image are converted to one of two values (a high and a low value).
21
+ Otsu's method is used for automatic thresholding.
22
+ See https://docs.opencv.org/4.x/d7/d4d/tutorial_py_thresholding.html.
23
+ The threshold is increased by ADDITIONAL_BINARY_THRESH in order to get more of
24
+ the centre of the beam.
25
+ """
26
+ max_pixel_value = 255
27
+
28
+ blurred_image = convert_to_gray_and_blur(image)
29
+
30
+ threshold_value, _ = cv2.threshold(
31
+ blurred_image, 0, max_pixel_value, cv2.THRESH_BINARY + cv2.THRESH_OTSU
32
+ )
33
+
34
+ # Adjusting because the inner beam is less noisy compared to the outer
35
+ threshold_value += ADDITIONAL_BINARY_THRESH
36
+
37
+ _, thresholded_image = cv2.threshold(
38
+ blurred_image, threshold_value, max_pixel_value, cv2.THRESH_BINARY
39
+ )
40
+
41
+ LOGGER.info(f"Image binarised with threshold of {threshold_value}")
42
+ return thresholded_image
43
+
44
+
45
+ class CentreEllipseMethod(StandardReadable, Triggerable):
46
+ """
47
+ Upon triggering, fits an ellipse to a binary image from the area detector defined by
48
+ the prefix.
49
+
50
+ This is used, in conjunction with a scintillator, to determine the centre of the beam
51
+ on the image.
52
+ """
53
+
54
+ def __init__(self, prefix: str, name: str = ""):
55
+ self.oav_array_signal = epics_signal_r(np.ndarray, f"pva://{prefix}PVA:ARRAY")
56
+
57
+ self.center_x_val, self._center_x_val_setter = soft_signal_r_and_setter(float)
58
+ self.center_y_val, self._center_y_val_setter = soft_signal_r_and_setter(float)
59
+ super().__init__(name)
60
+
61
+ def _fit_ellipse(self, binary_img: cv2.typing.MatLike) -> cv2.typing.RotatedRect:
62
+ contours, _ = cv2.findContours(
63
+ binary_img, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE
64
+ )
65
+ if not contours:
66
+ raise ValueError("No contours found in image.")
67
+
68
+ largest_contour = max(contours, key=cv2.contourArea)
69
+ if len(largest_contour) < 5:
70
+ raise ValueError(
71
+ f"Not enough points to fit an ellipse. Found {largest_contour} points and need at least 5."
72
+ )
73
+
74
+ return cv2.fitEllipse(largest_contour)
75
+
76
+ @AsyncStatus.wrap
77
+ async def trigger(self):
78
+ array_data = await self.oav_array_signal.get_value()
79
+ binary = convert_image_to_binary(array_data)
80
+ ellipse_fit = self._fit_ellipse(binary)
81
+ centre_x = ellipse_fit[0][0]
82
+ centre_y = ellipse_fit[0][1]
83
+ self._center_x_val_setter(centre_x)
84
+ self._center_y_val_setter(centre_y)
@@ -1,4 +1,3 @@
1
- import cv2
2
1
  import numpy as np
3
2
  from bluesky.protocols import Triggerable
4
3
  from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_r_and_setter
@@ -6,9 +5,7 @@ from ophyd_async.epics.core import (
6
5
  epics_signal_r,
7
6
  )
8
7
 
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)
8
+ from dodal.devices.oav.utils import convert_to_gray_and_blur
12
9
 
13
10
 
14
11
  class MaxPixel(StandardReadable, Triggerable):
@@ -19,20 +16,10 @@ class MaxPixel(StandardReadable, Triggerable):
19
16
  self.max_pixel_val, self._max_val_setter = soft_signal_r_and_setter(float)
20
17
  super().__init__(name)
21
18
 
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
19
  @AsyncStatus.wrap
34
20
  async def trigger(self):
35
- arr = await self._convert_to_gray_and_blur()
36
- max_val = float(np.max(arr)) # np.int64
21
+ img_data = await self.array_data.get_value()
22
+ arr = convert_to_gray_and_blur(img_data)
23
+ max_val = float(np.max(arr))
37
24
  assert isinstance(max_val, float)
38
25
  self._max_val_setter(max_val)
@@ -13,7 +13,7 @@ from ophyd_async.core import (
13
13
  soft_signal_r_and_setter,
14
14
  soft_signal_rw,
15
15
  )
16
- from redis.asyncio import StrictRedis
16
+ from redis.asyncio import ConnectionError, StrictRedis
17
17
 
18
18
  from dodal.devices.i04.constants import RedisConstants
19
19
  from dodal.devices.oav.oav_calculations import (
@@ -103,13 +103,25 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
103
103
  self.z_mm, self._z_mm_setter = soft_signal_r_and_setter(float)
104
104
  super().__init__(name=name)
105
105
 
106
+ async def _check_redis_connection(self):
107
+ try:
108
+ await self.redis_client.ping() # type: ignore
109
+ return True
110
+ except ConnectionError:
111
+ LOGGER.warning(
112
+ f"Failed to connect to redis: {self.redis_client}. Murko results device will not trigger"
113
+ )
114
+ return False
115
+
106
116
  def _reset(self):
107
117
  self._last_omega = None
108
118
  self._results: list[MurkoResult] = []
109
119
 
110
120
  @AsyncStatus.wrap
111
121
  async def stage(self):
112
- await self.pubsub.subscribe("murko-results")
122
+ self.redis_connected = await self._check_redis_connection()
123
+ if self.redis_connected:
124
+ await self.pubsub.subscribe("murko-results")
113
125
  self._x_mm_setter(0)
114
126
  self._y_mm_setter(0)
115
127
  self._z_mm_setter(0)
@@ -117,10 +129,13 @@ class MurkoResultsDevice(StandardReadable, Triggerable, Stageable):
117
129
  @AsyncStatus.wrap
118
130
  async def unstage(self):
119
131
  self._reset()
120
- await self.pubsub.unsubscribe()
132
+ if self.redis_connected:
133
+ await self.pubsub.unsubscribe()
121
134
 
122
135
  @AsyncStatus.wrap
123
136
  async def trigger(self):
137
+ if not self.redis_connected:
138
+ return
124
139
  sample_id = await self.sample_id.get_value()
125
140
  t_last_result = time.time()
126
141
  while True:
@@ -1,14 +1,3 @@
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
1
  J09_GAP_POLY_DEG_COLUMNS = [
13
2
  "9th-order",
14
3
  "8th-order",
@@ -23,64 +12,3 @@ J09_GAP_POLY_DEG_COLUMNS = [
23
12
  ]
24
13
 
25
14
  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,18 +11,18 @@ from ophyd_async.core import (
11
11
  soft_signal_rw,
12
12
  )
13
13
 
14
- from dodal.devices.insertion_device.apple2_undulator import (
14
+ from dodal.devices.insertion_device import (
15
15
  MAXIMUM_MOVE_TIME,
16
16
  Apple2,
17
17
  Apple2Controller,
18
18
  Apple2PhasesVal,
19
19
  Apple2Val,
20
- Pol,
21
20
  UndulatorGap,
22
21
  UndulatorJawPhase,
23
22
  UndulatorPhaseAxes,
24
23
  )
25
24
  from dodal.devices.insertion_device.energy_motor_lookup import EnergyMotorLookup
25
+ from dodal.devices.insertion_device.enum import Pol
26
26
 
27
27
  ROW_PHASE_MOTOR_TOLERANCE = 0.004
28
28
  MAXIMUM_ROW_PHASE_MOTOR_POSITION = 24.0
@@ -141,12 +141,12 @@ class I10Apple2Controller(Apple2Controller[I10Apple2]):
141
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
143
  return Apple2Val(
144
- gap=f"{gap:.6f}",
144
+ gap=gap,
145
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",
146
+ top_outer=phase,
147
+ top_inner=0.0,
148
+ btm_inner=phase3,
149
+ btm_outer=0.0,
150
150
  ),
151
151
  )
152
152
 
@@ -1,4 +1,4 @@
1
- from dodal.devices.insertion_device.apple2_undulator import (
1
+ from dodal.devices.insertion_device import (
2
2
  Apple2,
3
3
  Apple2Controller,
4
4
  Apple2PhasesVal,
@@ -56,11 +56,11 @@ class I17Apple2Controller(Apple2Controller[Apple2[UndulatorPhaseAxes]]):
56
56
 
57
57
  def _get_apple2_value(self, gap: float, phase: float, pol: Pol) -> Apple2Val:
58
58
  return Apple2Val(
59
- gap=f"{gap:.6f}",
59
+ gap=gap,
60
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}",
61
+ top_outer=phase,
62
+ top_inner=0.0,
63
+ btm_inner=phase,
64
+ btm_outer=0.0,
65
65
  ),
66
66
  )
@@ -1,3 +1,5 @@
1
1
  from dodal.devices.i21.enums import Grating
2
2
 
3
- __all__ = ["Grating"]
3
+ __all__ = [
4
+ "Grating",
5
+ ]
@@ -6,11 +6,10 @@ from bluesky.protocols import StreamAsset
6
6
  from event_model import DataKey # type: ignore
7
7
  from ophyd_async.core import (
8
8
  AsyncStatus,
9
- AutoIncrementingPathProvider,
10
9
  DetectorWriter,
10
+ PathProvider,
11
11
  StandardDetector,
12
12
  StandardReadable,
13
- StaticPathProvider,
14
13
  TriggerInfo,
15
14
  observe_value,
16
15
  wait_for_value,
@@ -22,7 +21,7 @@ from ophyd_async.fastcs.jungfrau._signals import JungfrauDriverIO
22
21
  from dodal.log import LOGGER
23
22
 
24
23
 
25
- class JunfrauCommissioningWriter(DetectorWriter, StandardReadable):
24
+ class JungfrauCommissioningWriter(DetectorWriter, StandardReadable):
26
25
  """Implementation of the temporary filewriter used for Jungfrau commissioning on i24.
27
26
 
28
27
  The PVs on this device are responsible for writing files of a specified name
@@ -33,11 +32,11 @@ class JunfrauCommissioningWriter(DetectorWriter, StandardReadable):
33
32
  def __init__(
34
33
  self,
35
34
  prefix,
36
- path_provider: AutoIncrementingPathProvider | StaticPathProvider,
35
+ path_provider: PathProvider,
37
36
  name="",
38
37
  ) -> None:
39
38
  with self.add_children_as_readables():
40
- self._path_info = path_provider
39
+ self._path_provider = path_provider
41
40
  self.frame_counter = epics_signal_rw(int, f"{prefix}NumCaptured")
42
41
  self.file_name = epics_signal_rw_rbv(str, f"{prefix}FileName")
43
42
  self.file_path = epics_signal_rw_rbv(str, f"{prefix}FilePath")
@@ -47,9 +46,8 @@ class JunfrauCommissioningWriter(DetectorWriter, StandardReadable):
47
46
 
48
47
  async def open(self, name: str, exposures_per_event: int = 1) -> dict[str, DataKey]:
49
48
  self._exposures_per_event = exposures_per_event
50
- _path_info = self._path_info()
49
+ _path_info = self._path_provider()
51
50
 
52
- # Commissioning Jungfrau plans allow you to override path, so check to see if file exists
53
51
  requested_filepath = Path(_path_info.directory_path) / _path_info.filename
54
52
  if requested_filepath.exists():
55
53
  raise FileExistsError(
@@ -65,6 +63,7 @@ class JunfrauCommissioningWriter(DetectorWriter, StandardReadable):
65
63
  f"Jungfrau writing to folder {_path_info.directory_path} with filename {_path_info.filename}"
66
64
  )
67
65
  await wait_for_value(self.writer_ready, 1, timeout=10)
66
+ self.final_path = requested_filepath
68
67
  return await self._describe()
69
68
 
70
69
  async def _describe(self) -> dict[str, DataKey]:
@@ -99,7 +98,7 @@ class JunfrauCommissioningWriter(DetectorWriter, StandardReadable):
99
98
 
100
99
 
101
100
  class CommissioningJungfrau(
102
- StandardDetector[JungfrauController, JunfrauCommissioningWriter]
101
+ StandardDetector[JungfrauController, JungfrauCommissioningWriter]
103
102
  ):
104
103
  """Ophyd-async implementation of a Jungfrau 9M Detector, using a temporary
105
104
  filewriter in place of Odin"""
@@ -108,11 +107,11 @@ class CommissioningJungfrau(
108
107
  self,
109
108
  prefix: str,
110
109
  writer_prefix: str,
111
- path_provider: AutoIncrementingPathProvider | StaticPathProvider,
110
+ path_provider: PathProvider,
112
111
  name="",
113
112
  ):
114
113
  self.drv = JungfrauDriverIO(prefix)
115
- writer = JunfrauCommissioningWriter(writer_prefix, path_provider)
114
+ writer = JungfrauCommissioningWriter(writer_prefix, path_provider)
116
115
  controller = JungfrauController(self.drv)
117
116
  super().__init__(controller, writer, name=name)
118
117
 
@@ -0,0 +1,62 @@
1
+ from .apple2_controller import (
2
+ MAXIMUM_MOVE_TIME,
3
+ Apple2Controller,
4
+ Apple2EnforceLHMoveController,
5
+ EnergyMotorConvertor,
6
+ )
7
+ from .apple2_undulator import (
8
+ DEFAULT_MOTOR_MIN_TIMEOUT,
9
+ Apple2,
10
+ Apple2LockedPhasesVal,
11
+ Apple2PhasesVal,
12
+ Apple2Val,
13
+ EnabledDisabledUpper,
14
+ UndulatorGap,
15
+ UndulatorJawPhase,
16
+ UndulatorLockedPhaseAxes,
17
+ UndulatorPhaseAxes,
18
+ UnstoppableMotor,
19
+ )
20
+ from .energy import BeamEnergy, InsertionDeviceEnergy, InsertionDeviceEnergyBase
21
+ from .energy_motor_lookup import (
22
+ ConfigServerEnergyMotorLookup,
23
+ EnergyMotorLookup,
24
+ )
25
+ from .enum import Pol, UndulatorGateStatus
26
+ from .lookup_table_models import (
27
+ EnergyCoverage,
28
+ LookupTable,
29
+ LookupTableColumnConfig,
30
+ convert_csv_to_lookup,
31
+ )
32
+ from .polarisation import InsertionDevicePolarisation
33
+
34
+ __all__ = [
35
+ "Apple2",
36
+ "Apple2Controller",
37
+ "Apple2EnforceLHMoveController",
38
+ "UndulatorGap",
39
+ "UndulatorPhaseAxes",
40
+ "UndulatorJawPhase",
41
+ "Apple2Val",
42
+ "Apple2PhasesVal",
43
+ "MAXIMUM_MOVE_TIME",
44
+ "LookupTable",
45
+ "LookupTableColumnConfig",
46
+ "convert_csv_to_lookup",
47
+ "InsertionDeviceEnergy",
48
+ "InsertionDevicePolarisation",
49
+ "BeamEnergy",
50
+ "UndulatorLockedPhaseAxes",
51
+ "EnergyCoverage",
52
+ "Pol",
53
+ "DEFAULT_MOTOR_MIN_TIMEOUT",
54
+ "EnabledDisabledUpper",
55
+ "UndulatorGateStatus",
56
+ "Apple2LockedPhasesVal",
57
+ "EnergyMotorLookup",
58
+ "ConfigServerEnergyMotorLookup",
59
+ "EnergyMotorConvertor",
60
+ "UnstoppableMotor",
61
+ "InsertionDeviceEnergyBase",
62
+ ]