dls-dodal 1.53.0__py3-none-any.whl → 1.55.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 (74) hide show
  1. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/METADATA +4 -6
  2. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/RECORD +74 -66
  3. dodal/_version.py +16 -3
  4. dodal/beamline_specific_utils/i05_shared.py +11 -0
  5. dodal/beamlines/__init__.py +1 -0
  6. dodal/beamlines/aithre.py +2 -3
  7. dodal/beamlines/b01_1.py +2 -2
  8. dodal/beamlines/b07.py +6 -3
  9. dodal/beamlines/b07_1.py +17 -6
  10. dodal/beamlines/b16.py +0 -1
  11. dodal/beamlines/b21.py +4 -6
  12. dodal/beamlines/i03.py +10 -31
  13. dodal/beamlines/i04.py +19 -45
  14. dodal/beamlines/i05.py +22 -0
  15. dodal/beamlines/i05_1.py +22 -0
  16. dodal/beamlines/i09.py +7 -3
  17. dodal/beamlines/i09_1.py +6 -3
  18. dodal/beamlines/i13_1.py +0 -1
  19. dodal/beamlines/i19_1.py +1 -2
  20. dodal/beamlines/i19_2.py +0 -1
  21. dodal/beamlines/i19_optics.py +1 -4
  22. dodal/beamlines/i20_1.py +38 -9
  23. dodal/beamlines/i23.py +1 -2
  24. dodal/beamlines/i24.py +0 -12
  25. dodal/beamlines/p60.py +13 -3
  26. dodal/common/beamlines/beamline_parameters.py +1 -1
  27. dodal/common/beamlines/device_helpers.py +0 -33
  28. dodal/devices/aithre_lasershaping/__init__.py +0 -0
  29. dodal/devices/aithre_lasershaping/goniometer.py +3 -26
  30. dodal/devices/aithre_lasershaping/laser_robot.py +2 -2
  31. dodal/devices/b07/__init__.py +2 -2
  32. dodal/devices/b07/enums.py +15 -0
  33. dodal/devices/b07_1/__init__.py +10 -1
  34. dodal/devices/b07_1/ccmc.py +79 -0
  35. dodal/devices/b07_1/enums.py +3 -0
  36. dodal/devices/electron_analyser/abstract/base_driver_io.py +25 -48
  37. dodal/devices/electron_analyser/abstract/base_region.py +9 -11
  38. dodal/devices/electron_analyser/abstract/types.py +12 -0
  39. dodal/devices/electron_analyser/specs/detector.py +9 -9
  40. dodal/devices/electron_analyser/specs/driver_io.py +54 -21
  41. dodal/devices/electron_analyser/specs/region.py +13 -8
  42. dodal/devices/electron_analyser/types.py +15 -6
  43. dodal/devices/electron_analyser/vgscienta/detector.py +18 -8
  44. dodal/devices/electron_analyser/vgscienta/driver_io.py +62 -24
  45. dodal/devices/electron_analyser/vgscienta/region.py +33 -16
  46. dodal/devices/focusing_mirror.py +1 -1
  47. dodal/devices/i03/undulator_dcm.py +8 -3
  48. dodal/devices/i05/__init__.py +3 -0
  49. dodal/devices/i05/enums.py +8 -0
  50. dodal/devices/i09/__init__.py +2 -2
  51. dodal/devices/i09/enums.py +16 -0
  52. dodal/devices/i09_1/__init__.py +2 -2
  53. dodal/devices/i09_1/enums.py +13 -0
  54. dodal/devices/i10/mirrors.py +2 -6
  55. dodal/devices/i13_1/merlin_controller.py +1 -1
  56. dodal/devices/i19/beamstop.py +2 -2
  57. dodal/devices/i24/aperture.py +1 -1
  58. dodal/devices/motors.py +75 -1
  59. dodal/devices/oav/oav_to_redis_forwarder.py +1 -1
  60. dodal/devices/oav/pin_image_recognition/__init__.py +1 -2
  61. dodal/devices/p60/__init__.py +8 -2
  62. dodal/devices/p60/enums.py +16 -0
  63. dodal/devices/robot.py +7 -4
  64. dodal/devices/tetramm.py +1 -2
  65. dodal/devices/webcam.py +2 -1
  66. dodal/devices/zebra/zebra.py +1 -1
  67. dodal/devices/zebra/zebra_controlled_shutter.py +1 -1
  68. dodal/devices/zocalo/__init__.py +2 -0
  69. dodal/devices/zocalo/zocalo_results.py +16 -26
  70. dodal/utils.py +3 -10
  71. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/WHEEL +0 -0
  72. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/entry_points.txt +0 -0
  73. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/licenses/LICENSE +0 -0
  74. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/top_level.txt +0 -0
@@ -6,7 +6,7 @@ from dodal.devices.motors import XYZStage
6
6
 
7
7
  class HomeGroup(StrictEnum):
8
8
  NONE = "none"
9
- ALL = "All"
9
+ ALL = "ALL"
10
10
  X = "X"
11
11
  Y = "Y"
12
12
  Z = "Z"
@@ -23,4 +23,4 @@ class BeamStop(XYZStage):
23
23
  def __init__(self, prefix: str, name: str = "") -> None:
24
24
  self.homing = HomingControl(f"{prefix}HM", name)
25
25
 
26
- super().__init__(name)
26
+ super().__init__(prefix, name)
@@ -22,4 +22,4 @@ class Aperture(XYStage):
22
22
 
23
23
  def __init__(self, prefix: str, name: str = "") -> None:
24
24
  self.position = epics_signal_rw(AperturePositions, prefix + "MP:SELECT")
25
- super().__init__(name)
25
+ super().__init__(prefix, name)
dodal/devices/motors.py CHANGED
@@ -1,6 +1,8 @@
1
+ import asyncio
2
+ import math
1
3
  from abc import ABC
2
4
 
3
- from ophyd_async.core import StandardReadable
5
+ from ophyd_async.core import StandardReadable, derived_signal_rw
4
6
  from ophyd_async.epics.motor import Motor
5
7
 
6
8
  _X, _Y, _Z = "X", "Y", "Z"
@@ -89,6 +91,25 @@ class XYPitchStage(XYStage):
89
91
  super().__init__(prefix, name, x_infix, y_infix)
90
92
 
91
93
 
94
+ class XYZPitchYawRollStage(XYZStage):
95
+ def __init__(
96
+ self,
97
+ prefix: str,
98
+ name: str = "",
99
+ x_infix: str = _X,
100
+ y_infix: str = _Y,
101
+ z_infix: str = _Z,
102
+ pitch_infix: str = "PITCH",
103
+ yaw_infix: str = "YAW",
104
+ roll_infix: str = "ROLL",
105
+ ):
106
+ with self.add_children_as_readables():
107
+ self.pitch = Motor(prefix + pitch_infix)
108
+ self.yaw = Motor(prefix + yaw_infix)
109
+ self.roll = Motor(prefix + roll_infix)
110
+ super().__init__(prefix, name, x_infix, y_infix, z_infix)
111
+
112
+
92
113
  class SixAxisGonio(XYZStage):
93
114
  def __init__(
94
115
  self,
@@ -101,12 +122,19 @@ class SixAxisGonio(XYZStage):
101
122
  phi_infix: str = "PHI",
102
123
  omega_infix: str = "OMEGA",
103
124
  ):
125
+ """Six-axis goniometer with a standard xyz stage and three axes of rotation:
126
+ kappa, phi and omega.
127
+ """
104
128
  with self.add_children_as_readables():
105
129
  self.kappa = Motor(prefix + kappa_infix)
106
130
  self.phi = Motor(prefix + phi_infix)
107
131
  self.omega = Motor(prefix + omega_infix)
108
132
  super().__init__(prefix, name, x_infix, y_infix, z_infix)
109
133
 
134
+ self.vertical_in_lab_space = create_axis_perp_to_rotation(
135
+ self.omega, self.y, self.z
136
+ )
137
+
110
138
 
111
139
  class YZStage(Stage):
112
140
  def __init__(
@@ -116,3 +144,49 @@ class YZStage(Stage):
116
144
  self.y = Motor(prefix + y_infix)
117
145
  self.z = Motor(prefix + z_infix)
118
146
  super().__init__(name)
147
+
148
+
149
+ def create_axis_perp_to_rotation(motor_theta: Motor, motor_i: Motor, motor_j: Motor):
150
+ """Given a signal that controls a motor in a rotation axis and two other
151
+ signals controlling motors on a pair of orthogonal axes, these axes being in the
152
+ rotating frame of reference created by the first axis, create a derived signal
153
+ that is a projection of the two axes in the non-rotating frame of reference.
154
+
155
+ The projection is onto the axis defined by i when the rotation angle is 0 and
156
+ defined by j when the angle is at 90.
157
+
158
+ The usual use case for this is translating from sample space to lab space. For
159
+ example, if you have a sample that is mounted on a goniometer to the right hand side
160
+ of an OAV view this can provide an axis that will move the sample up/down in that
161
+ view regardless of the omega orientation of the sample.
162
+
163
+ Args:
164
+ motor_theta (Motor): this is the rotation axis of the sample.
165
+ motor_i (Motor): this is the axis that, when the sample is at 0 deg rotation,
166
+ a move here is entirely parallel with the derived axis.
167
+ motor_j (Motor): this is the axis that, when the sample is at 90 deg rotation,
168
+ a move here is entirely parallel with the derived axis.
169
+ """
170
+
171
+ def _get(j_val: float, i_val: float, rot_value: float) -> float:
172
+ i_component = i_val * math.cos(math.radians(rot_value))
173
+ j_component = j_val * math.sin(math.radians(rot_value))
174
+ return i_component + j_component
175
+
176
+ async def _set(vertical_value: float) -> None:
177
+ rot_value = await motor_theta.user_readback.get_value()
178
+ i_component = vertical_value * math.cos(math.radians(rot_value))
179
+ j_component = vertical_value * math.sin(math.radians(rot_value))
180
+ await asyncio.gather(
181
+ motor_i.set(i_component),
182
+ motor_j.set(j_component),
183
+ motor_theta.set(rot_value),
184
+ )
185
+
186
+ return derived_signal_rw(
187
+ _get,
188
+ _set,
189
+ i_val=motor_i,
190
+ j_val=motor_j,
191
+ rot_value=motor_theta,
192
+ )
@@ -110,11 +110,11 @@ class OAVToRedisForwarder(StandardReadable, Flyable, Stoppable):
110
110
  pickled numpy array of pixel values but raw byes are more space efficient. There
111
111
  may be better ways of doing this, see https://github.com/DiamondLightSource/mx-bluesky/issues/592"""
112
112
  jpeg_bytes = await get_next_jpeg(response)
113
- self.uuid_setter(redis_uuid)
114
113
  sample_id = await self.sample_id.get_value()
115
114
  redis_key = f"murko:{sample_id}:raw"
116
115
  await self.redis_client.hset(redis_key, redis_uuid, jpeg_bytes) # type: ignore
117
116
  await self.redis_client.expire(redis_key, timedelta(days=self.DATA_EXPIRY_DAYS))
117
+ self.uuid_setter(redis_uuid)
118
118
 
119
119
  async def _open_connection_and_do_function(
120
120
  self, function_to_do: Callable[[ClientResponse, OAVSource], Awaitable]
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  import time
3
2
 
4
3
  import numpy as np
@@ -160,7 +159,7 @@ class PinTipDetection(StandardReadable):
160
159
  )
161
160
  else:
162
161
  break
163
- except asyncio.exceptions.TimeoutError:
162
+ except TimeoutError:
164
163
  LOGGER.error(
165
164
  f"No tip found in {await self.validity_timeout.get_value()} seconds."
166
165
  )
@@ -1,4 +1,10 @@
1
- from .enums import LensMode
1
+ from .enums import LensMode, PassEnergy, PsuMode
2
2
  from .lab_xray_source import LabXraySource, LabXraySourceReadable
3
3
 
4
- __all__ = ["LensMode", "LabXraySource", "LabXraySourceReadable"]
4
+ __all__ = [
5
+ "LensMode",
6
+ "PsuMode",
7
+ "PassEnergy",
8
+ "LabXraySource",
9
+ "LabXraySourceReadable",
10
+ ]
@@ -8,3 +8,19 @@ class LensMode(StrictEnum):
8
8
  ANGULAR30 = "Angular30"
9
9
  ANGULAR30_SMALLSPOT = "Angular30_SmallSpot"
10
10
  ANGULAR14_SMALLSPOT = "Angular14_SmallSpot"
11
+
12
+
13
+ class PsuMode(StrictEnum):
14
+ HIGH = "High Pass (XPS)"
15
+ LOW = "Low Pass (UPS)"
16
+
17
+
18
+ class PassEnergy(StrictEnum):
19
+ E1 = 1
20
+ E2 = 2
21
+ E5 = 5
22
+ E10 = 10
23
+ E20 = 20
24
+ E50 = 50
25
+ E100 = 100
26
+ E200 = 200
dodal/devices/robot.py CHANGED
@@ -21,6 +21,9 @@ from ophyd_async.epics.core import (
21
21
 
22
22
  from dodal.log import LOGGER
23
23
 
24
+ WAIT_FOR_OLD_PIN_MSG = "Waiting on old pin unloaded"
25
+ WAIT_FOR_NEW_PIN_MSG = "Waiting on new pin loaded"
26
+
24
27
 
25
28
  class RobotLoadFailed(Exception):
26
29
  error_code: int
@@ -74,7 +77,7 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
74
77
  # How far the gonio position can be out before loading will fail
75
78
  LOAD_TOLERANCE_MM = 0.02
76
79
 
77
- def __init__(self, name: str, prefix: str) -> None:
80
+ def __init__(self, prefix: str, name: str = "") -> None:
78
81
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
79
82
  self.barcode = epics_signal_r(str, prefix + "BARCODE")
80
83
  self.gonio_pin_sensor = epics_signal_r(PinMounted, prefix + "PIN_MOUNTED")
@@ -144,6 +147,7 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
144
147
  # in the current task, when it propagates to here we should cancel all pending tasks before bubbling up
145
148
  for task in tasks:
146
149
  task.cancel()
150
+
147
151
  raise
148
152
 
149
153
  async def _load_pin_and_puck(self, sample_location: SampleLocation):
@@ -164,9 +168,9 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
164
168
  )
165
169
  await self.load.trigger()
166
170
  if await self.gonio_pin_sensor.get_value() == PinMounted.PIN_MOUNTED:
167
- LOGGER.info("Waiting on old pin unloaded")
171
+ LOGGER.info(WAIT_FOR_OLD_PIN_MSG)
168
172
  await wait_for_value(self.gonio_pin_sensor, PinMounted.NO_PIN_MOUNTED, None)
169
- LOGGER.info("Waiting on new pin loaded")
173
+ LOGGER.info(WAIT_FOR_NEW_PIN_MSG)
170
174
 
171
175
  await self.pin_mounted_or_no_pin_found()
172
176
 
@@ -178,7 +182,6 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
178
182
  timeout=self.LOAD_TIMEOUT + self.NOT_BUSY_TIMEOUT,
179
183
  )
180
184
  except TimeoutError as e:
181
- # Will only need to catch asyncio.TimeoutError after https://github.com/bluesky/ophyd-async/issues/572
182
185
  await self.prog_error.raise_if_error(e)
183
186
  await self.controller_error.raise_if_error(e)
184
187
  raise RobotLoadFailed(0, "Robot timed out") from e
dodal/devices/tetramm.py CHANGED
@@ -22,9 +22,8 @@ from ophyd_async.epics.adcore import (
22
22
  NDArrayBaseIO,
23
23
  NDFileHDFIO,
24
24
  NDPluginBaseIO,
25
- stop_busy_record,
26
25
  )
27
- from ophyd_async.epics.core import PvSuffix
26
+ from ophyd_async.epics.core import PvSuffix, stop_busy_record
28
27
 
29
28
 
30
29
  class TetrammRange(StrictEnum):
dodal/devices/webcam.py CHANGED
@@ -12,6 +12,7 @@ from ophyd_async.core import (
12
12
  soft_signal_rw,
13
13
  )
14
14
  from PIL import Image
15
+ from yarl import URL
15
16
 
16
17
  from dodal.log import LOGGER
17
18
 
@@ -26,7 +27,7 @@ def create_placeholder_image() -> ByteString:
26
27
 
27
28
 
28
29
  class Webcam(StandardReadable, Triggerable):
29
- def __init__(self, name, prefix, url):
30
+ def __init__(self, url: URL, name: str = ""):
30
31
  self.url = url
31
32
  self.filename = soft_signal_rw(str, name="filename")
32
33
  self.directory = soft_signal_rw(str, name="directory")
@@ -286,7 +286,7 @@ class SoftInputs(StandardReadable):
286
286
  class Zebra(StandardReadable):
287
287
  """The Zebra device."""
288
288
 
289
- def __init__(self, mapping: ZebraMapping, name: str, prefix: str) -> None:
289
+ def __init__(self, mapping: ZebraMapping, prefix: str, name: str = "") -> None:
290
290
  self.mapping = mapping
291
291
  self.pc = PositionCompare(prefix, name)
292
292
  self.output = ZebraOutputPanel(prefix, name)
@@ -28,7 +28,7 @@ class ZebraShutter(StandardReadable, Movable[ZebraShutterState]):
28
28
  by a different soft input (aliased to manual_position_setpoint). Both these AND
29
29
  gates then feed into an OR gate, which then feeds to the shutter."""
30
30
 
31
- def __init__(self, prefix: str, name: str):
31
+ def __init__(self, prefix: str, name: str = ""):
32
32
  self._manual_position_setpoint = epics_signal_w(
33
33
  ZebraShutterState, prefix + "CTRL2"
34
34
  )
@@ -4,6 +4,7 @@ from dodal.devices.zocalo.zocalo_results import (
4
4
  NoZocaloSubscription,
5
5
  XrcResult,
6
6
  ZocaloResults,
7
+ ZocaloSource,
7
8
  get_full_processing_results,
8
9
  )
9
10
 
@@ -15,4 +16,5 @@ __all__ = [
15
16
  "NoResultsFromZocalo",
16
17
  "NoZocaloSubscription",
17
18
  "ZocaloStartInfo",
19
+ "ZocaloSource",
18
20
  ]
@@ -119,8 +119,7 @@ class ZocaloResults(StandardReadable, Triggerable):
119
119
 
120
120
  prefix (str): EPICS PV prefix for the device
121
121
 
122
- use_gpu (bool): When True, ZocaloResults will take the first set of
123
- results that it receives (which are likely the GPU results)
122
+ results_source (ZocaloSource): Where to get results from, GPU or CPU analysis
124
123
 
125
124
  """
126
125
 
@@ -132,7 +131,7 @@ class ZocaloResults(StandardReadable, Triggerable):
132
131
  sort_key: str = DEFAULT_SORT_KEY.value,
133
132
  timeout_s: float = DEFAULT_TIMEOUT,
134
133
  prefix: str = "",
135
- use_gpu: bool = False,
134
+ results_source: ZocaloSource = ZocaloSource.CPU,
136
135
  ) -> None:
137
136
  self.zocalo_environment = zocalo_environment
138
137
  self.sort_key = SortKeys[sort_key]
@@ -141,7 +140,7 @@ class ZocaloResults(StandardReadable, Triggerable):
141
140
  self._prefix = prefix
142
141
  self._raw_results_received: Queue = Queue()
143
142
  self.transport: CommonTransport | None = None
144
- self.use_gpu = use_gpu
143
+ self.results_source = results_source
145
144
 
146
145
  self.centre_of_mass, self._com_setter = soft_signal_r_and_setter(
147
146
  Array1D[np.float64], name="centre_of_mass"
@@ -237,9 +236,6 @@ class ZocaloResults(StandardReadable, Triggerable):
237
236
  "meant for it"
238
237
  )
239
238
  if not self.transport:
240
- LOGGER.warning(
241
- msg # AsyncStatus exception messages are poorly propagated, remove after https://github.com/bluesky/ophyd-async/issues/103
242
- )
243
239
  raise NoZocaloSubscription(msg)
244
240
 
245
241
  try:
@@ -247,16 +243,19 @@ class ZocaloResults(StandardReadable, Triggerable):
247
243
  f"waiting for results in queue - currently {self._raw_results_received.qsize()} items"
248
244
  )
249
245
 
250
- raw_results = self._raw_results_received.get(timeout=self.timeout_s)
251
- source_of_first_results = source_from_results(raw_results)
246
+ while True:
247
+ raw_results = self._raw_results_received.get(timeout=self.timeout_s)
248
+ source_of_results = source_from_results(raw_results)
252
249
 
253
- if self.use_gpu and source_of_first_results == ZocaloSource.CPU:
254
- LOGGER.warning(
255
- "Configured to use GPU results but CPU came first, using CPU results."
256
- )
250
+ if source_of_results != self.results_source:
251
+ LOGGER.warning(
252
+ f"Configured to use {self.results_source} results but {source_of_results} came first, waiting for further results."
253
+ )
254
+ else:
255
+ break
257
256
 
258
257
  LOGGER.info(
259
- f"Zocalo results from {source_from_results(raw_results)} processing: found {len(raw_results['results'])} crystals."
258
+ f"Zocalo results: found {len(raw_results['results'])} crystals."
260
259
  )
261
260
  # Sort from strongest to weakest in case of multiple crystals
262
261
  await self._put_results(
@@ -288,18 +287,9 @@ class ZocaloResults(StandardReadable, Triggerable):
288
287
 
289
288
  results = message.get("results", [])
290
289
 
291
- if self.use_gpu:
292
- self._raw_results_received.put(
293
- {"results": results, "recipe_parameters": recipe_parameters}
294
- )
295
- else:
296
- # Only add to queue if results are from CPU
297
- if not recipe_parameters.get("gpu"):
298
- self._raw_results_received.put(
299
- {"results": results, "recipe_parameters": recipe_parameters}
300
- )
301
- else:
302
- LOGGER.warning("Discarding results as they are from GPU")
290
+ self._raw_results_received.put(
291
+ {"results": results, "recipe_parameters": recipe_parameters}
292
+ )
303
293
 
304
294
  subscription = workflows.recipe.wrap_subscribe(
305
295
  self.transport,
dodal/utils.py CHANGED
@@ -426,16 +426,9 @@ def is_v2_device_type(obj: type[Any]) -> bool:
426
426
  # This is all very badly documented and possibly prone to change in future versions of Python
427
427
  non_parameterized_class = obj.__origin__
428
428
  if non_parameterized_class:
429
- try:
430
- return non_parameterized_class and issubclass(
431
- non_parameterized_class, OphydV2Device
432
- )
433
- except TypeError:
434
- # Python 3.10 will return inspect.isclass(t) == True but then
435
- # raise TypeError: issubclass() arg 1 must be a class
436
- # when inspecting device_factory decorator function itself
437
- # Later versions of Python seem not to be affected
438
- pass
429
+ return non_parameterized_class and issubclass(
430
+ non_parameterized_class, OphydV2Device
431
+ )
439
432
 
440
433
  return False
441
434