dls-dodal 1.46.0__py3-none-any.whl → 1.48.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 (81) hide show
  1. {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/RECORD +74 -63
  3. {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/__init__.py +0 -1
  6. dodal/beamlines/aithre.py +6 -0
  7. dodal/beamlines/b01_1.py +1 -1
  8. dodal/beamlines/b07.py +2 -6
  9. dodal/beamlines/b07_1.py +1 -3
  10. dodal/beamlines/i03.py +33 -21
  11. dodal/beamlines/i04.py +65 -26
  12. dodal/beamlines/i09.py +1 -3
  13. dodal/beamlines/i09_1.py +1 -3
  14. dodal/beamlines/i18.py +1 -1
  15. dodal/beamlines/i19_1.py +9 -6
  16. dodal/beamlines/i23.py +17 -1
  17. dodal/beamlines/i24.py +5 -5
  18. dodal/beamlines/p38.py +1 -1
  19. dodal/beamlines/p60.py +2 -6
  20. dodal/beamlines/p99.py +48 -4
  21. dodal/common/beamlines/beamline_parameters.py +3 -30
  22. dodal/common/data_util.py +4 -0
  23. dodal/devices/aithre_lasershaping/goniometer.py +36 -2
  24. dodal/devices/aithre_lasershaping/laser_robot.py +27 -0
  25. dodal/devices/aperturescatterguard.py +47 -47
  26. dodal/devices/current_amplifiers/struck_scaler_counter.py +1 -1
  27. dodal/devices/diamond_filter.py +5 -17
  28. dodal/devices/eiger.py +1 -1
  29. dodal/devices/electron_analyser/__init__.py +18 -0
  30. dodal/devices/electron_analyser/abstract/__init__.py +22 -0
  31. dodal/devices/electron_analyser/abstract/base_detector.py +223 -0
  32. dodal/devices/electron_analyser/abstract/base_driver_io.py +230 -0
  33. dodal/devices/electron_analyser/{abstract_region.py → abstract/base_region.py} +3 -9
  34. dodal/devices/electron_analyser/specs/__init__.py +10 -0
  35. dodal/devices/electron_analyser/specs/detector.py +13 -0
  36. dodal/devices/electron_analyser/specs/driver_io.py +89 -0
  37. dodal/devices/electron_analyser/{specs_region.py → specs/region.py} +1 -1
  38. dodal/devices/electron_analyser/types.py +6 -0
  39. dodal/devices/electron_analyser/util.py +13 -0
  40. dodal/devices/electron_analyser/vgscienta/__init__.py +11 -0
  41. dodal/devices/electron_analyser/vgscienta/detector.py +22 -0
  42. dodal/devices/electron_analyser/vgscienta/driver_io.py +67 -0
  43. dodal/devices/electron_analyser/{vgscienta_region.py → vgscienta/region.py} +1 -2
  44. dodal/devices/fast_grid_scan.py +7 -9
  45. dodal/devices/i03/__init__.py +3 -0
  46. dodal/devices/i04/__init__.py +3 -0
  47. dodal/devices/i04/constants.py +9 -0
  48. dodal/devices/i04/murko_results.py +192 -0
  49. dodal/devices/i10/diagnostics.py +9 -61
  50. dodal/devices/i18/diode.py +37 -4
  51. dodal/devices/i24/focus_mirrors.py +9 -13
  52. dodal/devices/i24/pilatus_metadata.py +9 -9
  53. dodal/devices/i24/pmac.py +19 -14
  54. dodal/devices/{i03 → mx_phase1}/beamstop.py +26 -15
  55. dodal/devices/oav/oav_calculations.py +2 -2
  56. dodal/devices/oav/oav_detector.py +80 -32
  57. dodal/devices/oav/oav_parameters.py +46 -16
  58. dodal/devices/oav/oav_to_redis_forwarder.py +2 -2
  59. dodal/devices/oav/utils.py +2 -2
  60. dodal/devices/p99/andor2_point.py +41 -0
  61. dodal/devices/positioner.py +49 -0
  62. dodal/devices/robot.py +20 -1
  63. dodal/devices/smargon.py +43 -4
  64. dodal/devices/tetramm.py +5 -2
  65. dodal/devices/util/adjuster_plans.py +1 -1
  66. dodal/devices/zebra/zebra.py +8 -0
  67. dodal/devices/zebra/zebra_constants_mapping.py +1 -1
  68. dodal/devices/zocalo/__init__.py +0 -3
  69. dodal/devices/zocalo/zocalo_results.py +6 -32
  70. dodal/log.py +14 -14
  71. dodal/plans/configure_arm_trigger_and_disarm_detector.py +167 -0
  72. dodal/common/signal_utils.py +0 -88
  73. dodal/devices/electron_analyser/abstract_analyser_io.py +0 -47
  74. dodal/devices/electron_analyser/specs_analyser_io.py +0 -19
  75. dodal/devices/electron_analyser/vgscienta_analyser_io.py +0 -26
  76. dodal/devices/logging_ophyd_device.py +0 -17
  77. dodal/plan_stubs/electron_analyser/__init__.py +0 -0
  78. dodal/plan_stubs/electron_analyser/configure_controller.py +0 -80
  79. {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/entry_points.txt +0 -0
  80. {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/licenses/LICENSE +0 -0
  81. {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/top_level.txt +0 -0
@@ -5,13 +5,20 @@ from ophyd_async.core import (
5
5
  DEFAULT_TIMEOUT,
6
6
  AsyncStatus,
7
7
  LazyMock,
8
+ SignalR,
8
9
  StandardReadable,
10
+ derived_signal_r,
11
+ soft_signal_rw,
9
12
  )
10
- from ophyd_async.epics.core import epics_signal_rw
13
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
11
14
 
12
- from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
13
15
  from dodal.devices.areadetector.plugins.CAM import Cam
14
- from dodal.devices.oav.oav_parameters import DEFAULT_OAV_WINDOW, OAVConfig
16
+ from dodal.devices.oav.oav_parameters import (
17
+ DEFAULT_OAV_WINDOW,
18
+ OAVConfig,
19
+ OAVConfigBase,
20
+ OAVConfigBeamCentre,
21
+ )
15
22
  from dodal.devices.oav.snapshots.snapshot import Snapshot
16
23
  from dodal.devices.oav.snapshots.snapshot_with_grid import SnapshotWithGrid
17
24
 
@@ -52,7 +59,10 @@ class ZoomController(StandardReadable, Movable[str]):
52
59
 
53
60
 
54
61
  class OAV(StandardReadable):
55
- def __init__(self, prefix: str, config: OAVConfig, name: str = ""):
62
+ beam_centre_i: SignalR[int]
63
+ beam_centre_j: SignalR[int]
64
+
65
+ def __init__(self, prefix: str, config: OAVConfigBase, name: str = ""):
56
66
  self.oav_config = config
57
67
  self._prefix = prefix
58
68
  self._name = name
@@ -60,51 +70,40 @@ class OAV(StandardReadable):
60
70
  self.zoom_controller = ZoomController(f"{_bl_prefix}-EA-OAV-01:FZOOM:", name)
61
71
 
62
72
  self.cam = Cam(f"{prefix}CAM:", name=name)
63
-
64
73
  with self.add_children_as_readables():
65
74
  self.grid_snapshot = SnapshotWithGrid(f"{prefix}MJPG:", name)
66
- self.microns_per_pixel_x = create_r_hardware_backed_soft_signal(
67
- float,
68
- lambda: self._get_microns_per_pixel(Coords.X),
69
- )
70
- self.microns_per_pixel_y = create_r_hardware_backed_soft_signal(
71
- float,
72
- lambda: self._get_microns_per_pixel(Coords.Y),
73
- )
74
- self.beam_centre_i = create_r_hardware_backed_soft_signal(
75
- int, lambda: self._get_beam_position(Coords.X)
75
+
76
+ self.sizes = [self.grid_snapshot.x_size, self.grid_snapshot.y_size]
77
+
78
+ with self.add_children_as_readables():
79
+ self.microns_per_pixel_x = derived_signal_r(
80
+ self._get_microns_per_pixel,
81
+ zoom_level=self.zoom_controller.level,
82
+ size=self.sizes[Coords.X],
83
+ coord=soft_signal_rw(datatype=int, initial_value=Coords.X.value),
76
84
  )
77
- self.beam_centre_j = create_r_hardware_backed_soft_signal(
78
- int, lambda: self._get_beam_position(Coords.Y)
85
+ self.microns_per_pixel_y = derived_signal_r(
86
+ self._get_microns_per_pixel,
87
+ zoom_level=self.zoom_controller.level,
88
+ size=self.sizes[Coords.Y],
89
+ coord=soft_signal_rw(datatype=int, initial_value=Coords.Y.value),
79
90
  )
80
91
  self.snapshot = Snapshot(
81
92
  f"{self._prefix}MJPG:",
82
93
  self._name,
83
94
  )
84
95
 
85
- self.sizes = [self.grid_snapshot.x_size, self.grid_snapshot.y_size]
86
-
87
96
  super().__init__(name)
88
97
 
89
- async def _read_current_zoom(self) -> str:
90
- _zoom = await self.zoom_controller.level.get_value()
98
+ def _read_current_zoom(self, _zoom: str) -> str:
91
99
  return _get_correct_zoom_string(_zoom)
92
100
 
93
- async def _get_microns_per_pixel(self, coord: int) -> float:
101
+ def _get_microns_per_pixel(self, zoom_level: str, size: int, coord: int) -> float:
94
102
  """Extracts the microns per x pixel and y pixel for a given zoom level."""
95
- _zoom = await self._read_current_zoom()
103
+ _zoom = self._read_current_zoom(zoom_level)
96
104
  value = self.parameters[_zoom].microns_per_pixel[coord]
97
- size = await self.sizes[coord].get_value()
98
105
  return value * DEFAULT_OAV_WINDOW[coord] / size
99
106
 
100
- async def _get_beam_position(self, coord: int) -> int:
101
- """Extracts the beam location in pixels `xCentre` `yCentre`, for a requested \
102
- zoom level. """
103
- _zoom = await self._read_current_zoom()
104
- value = self.parameters[_zoom].crosshair[coord]
105
- size = await self.sizes[coord].get_value()
106
- return int(value * size / DEFAULT_OAV_WINDOW[coord])
107
-
108
107
  async def connect(
109
108
  self,
110
109
  mock: bool | LazyMock = False,
@@ -114,3 +113,52 @@ class OAV(StandardReadable):
114
113
  self.parameters = self.oav_config.get_parameters()
115
114
 
116
115
  return await super().connect(mock, timeout, force_reconnect)
116
+
117
+
118
+ class OAVBeamCentreFile(OAV):
119
+ """OAV device that reads its beam centre values from a file. The config parameter
120
+ must be a OAVConfigBeamCentre object, as this contains a filepath to where the beam
121
+ centre values are stored.
122
+ """
123
+
124
+ def __init__(self, prefix: str, config: OAVConfigBeamCentre, name: str = ""):
125
+ super().__init__(prefix, config, name)
126
+
127
+ with self.add_children_as_readables():
128
+ self.beam_centre_i = derived_signal_r(
129
+ self._get_beam_position,
130
+ zoom_level=self.zoom_controller.level,
131
+ size=self.sizes[Coords.X],
132
+ coord=soft_signal_rw(datatype=int, initial_value=Coords.X.value),
133
+ )
134
+ self.beam_centre_j = derived_signal_r(
135
+ self._get_beam_position,
136
+ zoom_level=self.zoom_controller.level,
137
+ size=self.sizes[Coords.Y],
138
+ coord=soft_signal_rw(datatype=int, initial_value=Coords.Y.value),
139
+ )
140
+ # Set name so that new child signals get correct name
141
+ self.set_name(self.name)
142
+
143
+ def _get_beam_position(self, zoom_level: str, size: int, coord: int) -> int:
144
+ """Extracts the beam location in pixels `xCentre` `yCentre`, for a requested \
145
+ zoom level. """
146
+ _zoom = self._read_current_zoom(zoom_level)
147
+ value = self.parameters[_zoom].crosshair[coord]
148
+ return int(value * size / DEFAULT_OAV_WINDOW[coord])
149
+
150
+
151
+ class OAVBeamCentrePV(OAV):
152
+ """OAV device that reads its beam centre values from PVs."""
153
+
154
+ def __init__(
155
+ self, prefix: str, config: OAVConfig, name: str = "", overlay_channel: int = 1
156
+ ):
157
+ with self.add_children_as_readables():
158
+ self.beam_centre_i = epics_signal_r(
159
+ int, prefix + f"OVER:{overlay_channel}:CenterX"
160
+ )
161
+ self.beam_centre_j = epics_signal_r(
162
+ int, prefix + f"OVER:{overlay_channel}:CenterY"
163
+ )
164
+ super().__init__(prefix, config, name)
@@ -1,8 +1,9 @@
1
1
  import json
2
2
  import xml.etree.ElementTree as et
3
+ from abc import abstractmethod
3
4
  from collections import ChainMap
4
5
  from dataclasses import dataclass
5
- from typing import Any
6
+ from typing import Any, Generic, TypeVar
6
7
  from xml.etree.ElementTree import Element
7
8
 
8
9
  # GDA currently assumes this aspect ratio for the OAV window size.
@@ -107,22 +108,19 @@ class OAVParameters:
107
108
  @dataclass
108
109
  class ZoomParams:
109
110
  microns_per_pixel: tuple[float, float]
111
+
112
+
113
+ @dataclass
114
+ class ZoomParamsCrosshair(ZoomParams):
110
115
  crosshair: tuple[int, int]
111
116
 
112
117
 
113
- class OAVConfig:
114
- """ Read the OAV config files and return a dictionary of {'zoom_level': ZoomParams}\
115
- with information about microns per pixels and crosshairs.
116
- """
118
+ ParamType = TypeVar("ParamType", bound="ZoomParams")
117
119
 
118
- def __init__(self, zoom_params_file: str, display_config_file: str):
119
- self.zoom_params = self._get_zoom_params(zoom_params_file)
120
- self.display_config = self._get_display_config(display_config_file)
121
120
 
122
- def _get_display_config(self, display_config_file: str):
123
- with open(display_config_file) as f:
124
- file_lines = f.readlines()
125
- return file_lines
121
+ class OAVConfigBase(Generic[ParamType]):
122
+ def __init__(self, zoom_params_file: str):
123
+ self.zoom_params = self._get_zoom_params(zoom_params_file)
126
124
 
127
125
  def _get_zoom_params(self, zoom_params_file: str):
128
126
  tree = et.parse(zoom_params_file)
@@ -138,6 +136,39 @@ class OAVConfig:
138
136
  um_per_pix[zoom] = (um_pix_x, um_pix_y)
139
137
  return um_per_pix
140
138
 
139
+ @abstractmethod
140
+ def get_parameters(self) -> dict[str, ParamType]: ...
141
+
142
+
143
+ class OAVConfig(OAVConfigBase[ZoomParams]):
144
+ def get_parameters(self) -> dict[str, ZoomParams]:
145
+ config = {}
146
+ um_xy = self._read_zoom_params()
147
+ for zoom_key in list(um_xy.keys()):
148
+ config[zoom_key] = ZoomParams(
149
+ microns_per_pixel=um_xy[zoom_key],
150
+ )
151
+ return config
152
+
153
+
154
+ class OAVConfigBeamCentre(OAVConfigBase[ZoomParamsCrosshair]):
155
+ """ Read the OAV config files and return a dictionary of {'zoom_level': ZoomParams}\
156
+ with information about microns per pixels and crosshairs.
157
+ """
158
+
159
+ def __init__(
160
+ self,
161
+ zoom_params_file: str,
162
+ display_config_file: str,
163
+ ):
164
+ self.display_config = self._get_display_config(display_config_file)
165
+ super().__init__(zoom_params_file)
166
+
167
+ def _get_display_config(self, display_config_file: str):
168
+ with open(display_config_file) as f:
169
+ file_lines = f.readlines()
170
+ return file_lines
171
+
141
172
  def _read_display_config(self) -> dict:
142
173
  crosshairs = {}
143
174
  for i in range(len(self.display_config)):
@@ -148,13 +179,12 @@ class OAVConfig:
148
179
  crosshairs[zoom] = (x, y)
149
180
  return crosshairs
150
181
 
151
- def get_parameters(self) -> dict[str, ZoomParams]:
182
+ def get_parameters(self) -> dict[str, ZoomParamsCrosshair]:
152
183
  config = {}
153
184
  um_xy = self._read_zoom_params()
154
185
  bc_xy = self._read_display_config()
155
186
  for zoom_key in list(bc_xy.keys()):
156
- config[zoom_key] = ZoomParams(
157
- microns_per_pixel=um_xy[zoom_key],
158
- crosshair=bc_xy[zoom_key],
187
+ config[zoom_key] = ZoomParamsCrosshair(
188
+ microns_per_pixel=um_xy[zoom_key], crosshair=bc_xy[zoom_key]
159
189
  )
160
190
  return config
@@ -1,7 +1,7 @@
1
1
  import asyncio
2
2
  from collections.abc import Awaitable, Callable
3
3
  from datetime import timedelta
4
- from enum import Enum
4
+ from enum import IntEnum
5
5
  from uuid import uuid4
6
6
 
7
7
  from aiohttp import ClientResponse, ClientSession
@@ -29,7 +29,7 @@ async def get_next_jpeg(response: ClientResponse) -> bytes:
29
29
  return line + await response.content.readuntil(JPEG_STOP_BYTE)
30
30
 
31
31
 
32
- class Source(Enum):
32
+ class Source(IntEnum):
33
33
  FULL_SCREEN = 0
34
34
  ROI = 1
35
35
 
@@ -7,7 +7,7 @@ from bluesky.utils import Msg
7
7
 
8
8
  from dodal.devices.oav.oav_calculations import (
9
9
  calculate_beam_distance,
10
- camera_coordinates_to_xyz,
10
+ camera_coordinates_to_xyz_mm,
11
11
  )
12
12
  from dodal.devices.oav.oav_detector import OAV
13
13
  from dodal.devices.oav.pin_image_recognition import PinTipDetection
@@ -90,7 +90,7 @@ def calculate_x_y_z_of_pixel(
90
90
  """Get the x, y, z position of a pixel in mm"""
91
91
  beam_distance_px: Pixel = calculate_beam_distance(beam_centre, *pixel)
92
92
 
93
- return current_x_y_z + camera_coordinates_to_xyz(
93
+ return current_x_y_z + camera_coordinates_to_xyz_mm(
94
94
  beam_distance_px[0],
95
95
  beam_distance_px[1],
96
96
  current_omega,
@@ -0,0 +1,41 @@
1
+ from ophyd_async.core import (
2
+ StandardReadableFormat,
3
+ )
4
+ from ophyd_async.epics.adandor import Andor2DriverIO
5
+ from ophyd_async.epics.adcore import NDPluginBaseIO, SingleTriggerDetector
6
+ from ophyd_async.epics.core import epics_signal_r
7
+
8
+
9
+ class Andor2Point(SingleTriggerDetector):
10
+ """Using the andor2 as if it is a massive point detector, read the read uncached
11
+ value after a picture is taken."""
12
+
13
+ def __init__(
14
+ self,
15
+ prefix: str,
16
+ drv_suffix: str,
17
+ read_uncached: dict[str, str],
18
+ name: str = "",
19
+ plugins: dict[str, NDPluginBaseIO] | None = None,
20
+ ) -> None:
21
+ """
22
+ Parameters
23
+ ----------
24
+ prefix: str,
25
+ Beamline camera pv
26
+ drv_suffix : str,
27
+ Camera pv suffix
28
+ read_uncached: dict[str,str]
29
+ A dictionary contains the name and the pv suffix for the statistic plugin.
30
+ name: str:
31
+ Name of the device.
32
+ plugins:: Optional[dict[str, NDPluginBaseIO] | None
33
+ Dictionary containing plugin that are forward to the base class.
34
+ """
35
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
36
+ for k, v in read_uncached.items():
37
+ setattr(self, k, epics_signal_r(float, prefix + v))
38
+
39
+ super().__init__(
40
+ drv=Andor2DriverIO(prefix + drv_suffix), name=name, plugins=plugins
41
+ )
@@ -0,0 +1,49 @@
1
+ from typing import TypeVar
2
+
3
+ from bluesky.protocols import Movable
4
+ from ophyd_async.core import (
5
+ AsyncStatus,
6
+ StandardReadable,
7
+ StrictEnum,
8
+ )
9
+ from ophyd_async.epics.core import (
10
+ epics_signal_rw,
11
+ )
12
+ from ophyd_async.epics.motor import Motor
13
+
14
+ T = TypeVar("T", bound=StrictEnum)
15
+
16
+
17
+ class Positioner1D(StandardReadable, Movable[T]):
18
+ """1D stage with a enum table to select positions.
19
+
20
+ Use this when writing a device with an EPICS positioner on a single axis.
21
+ """
22
+
23
+ def __init__(
24
+ self,
25
+ prefix: str,
26
+ datatype: type[StrictEnum],
27
+ positioner_pv_suffix: str = ":MP:SELECT",
28
+ name: str = "",
29
+ ) -> None:
30
+ self._stage_motion = Motor(prefix=prefix)
31
+ with self.add_children_as_readables():
32
+ self.stage_position = epics_signal_rw(
33
+ datatype,
34
+ read_pv=prefix + positioner_pv_suffix,
35
+ )
36
+ super().__init__(name=name)
37
+
38
+ @AsyncStatus.wrap
39
+ async def set(self, value: T) -> None:
40
+ await self.stage_position.set(value=value)
41
+
42
+
43
+ def create_positioner(
44
+ datatype: type[T],
45
+ prefix,
46
+ positioner_pv_suffix: str = ":MP:SELECT",
47
+ name: str = "",
48
+ ) -> Positioner1D[T]:
49
+ return Positioner1D[datatype](prefix, datatype, positioner_pv_suffix, name)
dodal/devices/robot.py CHANGED
@@ -11,7 +11,12 @@ from ophyd_async.core import (
11
11
  set_and_wait_for_value,
12
12
  wait_for_value,
13
13
  )
14
- from ophyd_async.epics.core import epics_signal_r, epics_signal_rw_rbv, epics_signal_x
14
+ from ophyd_async.epics.core import (
15
+ epics_signal_r,
16
+ epics_signal_rw,
17
+ epics_signal_rw_rbv,
18
+ epics_signal_x,
19
+ )
15
20
 
16
21
  from dodal.log import LOGGER
17
22
 
@@ -88,6 +93,20 @@ class BartRobot(StandardReadable, Movable[SampleLocation]):
88
93
  self.controller_error = ErrorStatus(prefix + "CNTL")
89
94
 
90
95
  self.reset = epics_signal_x(prefix + "RESET.PROC")
96
+ self.stop = epics_signal_x(prefix + "ABORT.PROC")
97
+ self.init = epics_signal_x(prefix + "INIT.PROC")
98
+ self.soak = epics_signal_x(prefix + "SOAK.PROC")
99
+ self.home = epics_signal_x(prefix + "GOHM.PROC")
100
+ self.unload = epics_signal_x(prefix + "UNLD.PROC")
101
+ self.dry = epics_signal_x(prefix + "DRY.PROC")
102
+ self.open = epics_signal_x(prefix + "COLO.PROC")
103
+ self.close = epics_signal_x(prefix + "COLC.PROC")
104
+ self.cryomode_rbv = epics_signal_r(float, prefix + "CRYO_MODE_RBV")
105
+ self.cryomode = epics_signal_rw(str, prefix + "CRYO_MODE_CTRL")
106
+ self.gripper_temp = epics_signal_r(float, prefix + "GRIPPER_TEMP")
107
+ self.dewar_lid_temperature = epics_signal_rw(
108
+ float, prefix + "DW_1_TEMP", prefix + "DW_1_SET_POINT"
109
+ )
91
110
  super().__init__(name=name)
92
111
 
93
112
  async def pin_mounted_or_no_pin_found(self):
dodal/devices/smargon.py CHANGED
@@ -1,14 +1,23 @@
1
+ import asyncio
1
2
  from collections.abc import Collection, Generator
2
3
  from dataclasses import dataclass
3
4
  from enum import Enum
4
5
  from math import isclose
5
- from typing import cast
6
+ from typing import TypedDict, cast
6
7
 
7
8
  from bluesky import plan_stubs as bps
9
+ from bluesky.protocols import Movable
8
10
  from bluesky.utils import Msg
9
- from ophyd_async.core import AsyncStatus, Device, StandardReadable, wait_for_value
10
- from ophyd_async.epics.core import epics_signal_r
11
+ from ophyd_async.core import (
12
+ AsyncStatus,
13
+ Device,
14
+ StandardReadable,
15
+ StrictEnum,
16
+ wait_for_value,
17
+ )
18
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
11
19
  from ophyd_async.epics.motor import Motor
20
+ from typing_extensions import NotRequired
12
21
 
13
22
  from dodal.devices.util.epics_util import SetWhenEnabled
14
23
 
@@ -91,7 +100,23 @@ class XYZLimits:
91
100
  )
92
101
 
93
102
 
94
- class Smargon(StandardReadable):
103
+ class DeferMoves(StrictEnum):
104
+ ON = "Defer On"
105
+ OFF = "Defer Off"
106
+
107
+
108
+ class CombinedMove(TypedDict):
109
+ """A move on multiple axes at once using a deferred move"""
110
+
111
+ x: NotRequired[float | None]
112
+ y: NotRequired[float | None]
113
+ z: NotRequired[float | None]
114
+ omega: NotRequired[float | None]
115
+ phi: NotRequired[float | None]
116
+ chi: NotRequired[float | None]
117
+
118
+
119
+ class Smargon(StandardReadable, Movable):
95
120
  """
96
121
  Real motors added to allow stops following pin load (e.g. real_x1.stop() )
97
122
  X1 and X2 real motors provide compound chi motion as well as the compound X travel,
@@ -116,6 +141,8 @@ class Smargon(StandardReadable):
116
141
  self.stub_offsets = StubOffsets(prefix=prefix)
117
142
  self.disabled = epics_signal_r(int, prefix + "DISABLED")
118
143
 
144
+ self.defer_move = epics_signal_rw(DeferMoves, prefix + "CS1:DeferMoves")
145
+
119
146
  super().__init__(name)
120
147
 
121
148
  def get_xyz_limits(self) -> Generator[Msg, None, XYZLimits]:
@@ -135,3 +162,15 @@ class Smargon(StandardReadable):
135
162
  max_value = yield from bps.rd(pv.high_limit_travel)
136
163
  limits[name] = AxisLimit(min_value, max_value)
137
164
  return XYZLimits(**limits)
165
+
166
+ @AsyncStatus.wrap
167
+ async def set(self, value: CombinedMove):
168
+ await self.defer_move.set(DeferMoves.ON)
169
+ try:
170
+ tasks = []
171
+ for k, v in value.items():
172
+ if v is not None:
173
+ tasks.append(getattr(self, k).set(v))
174
+ await asyncio.gather(*tasks)
175
+ finally:
176
+ await self.defer_move.set(DeferMoves.OFF)
dodal/devices/tetramm.py CHANGED
@@ -131,11 +131,14 @@ class TetrammController(DetectorController):
131
131
  )
132
132
 
133
133
  async def arm(self):
134
- self._arm_status = await set_and_wait_for_value(self._drv.acquire, True)
134
+ self._arm_status = await set_and_wait_for_value(
135
+ self._drv.acquire, True, wait_for_set_completion=False
136
+ )
135
137
 
136
138
  async def wait_for_idle(self):
137
- if self._arm_status:
139
+ if self._arm_status and not self._arm_status.done:
138
140
  await self._arm_status
141
+ self._arm_status = None
139
142
 
140
143
  def _validate_trigger(self, trigger: DetectorTrigger) -> None:
141
144
  supported_trigger_types = {
@@ -20,6 +20,6 @@ def lookup_table_adjuster(
20
20
  def adjust(group=None) -> Generator[Msg, None, None]:
21
21
  setpoint = lookup_table(input)
22
22
  LOGGER.info(f"lookup_table_adjuster setting {output_device.name} to {setpoint}")
23
- yield from bps.abs_set(output_device, setpoint, group=group) # type: ignore # See: https://github.com/DiamondLightSource/dodal/issues/827
23
+ yield from bps.abs_set(output_device, setpoint, group=group)
24
24
 
25
25
  return adjust
@@ -68,6 +68,14 @@ class RotationDirection(StrictEnum):
68
68
  def multiplier(self):
69
69
  return 1 if self == RotationDirection.POSITIVE else -1
70
70
 
71
+ @property
72
+ def opposite(self) -> RotationDirection:
73
+ return (
74
+ RotationDirection.POSITIVE
75
+ if self == RotationDirection.NEGATIVE
76
+ else RotationDirection.NEGATIVE
77
+ )
78
+
71
79
 
72
80
  class ArmDemand(Enum):
73
81
  ARM = 1
@@ -89,7 +89,7 @@ class ZebraMapping(ZebraMappingValidations):
89
89
  # Which of the Zebra's four AND gates is used to control the automatic shutter, if it's being used.
90
90
  # After defining, the correct GateControl device can be accessed with, eg,
91
91
  # zebra.logic_gates.and_gates[zebra.mapping.AND_GATE_FOR_AUTO_SHUTTER]. Set to -1 if not being used.
92
- AND_GATE_FOR_AUTO_SHUTTER: int = Field(default=-1, ge=-1, le=4)
92
+ AND_GATE_FOR_AUTO_SHUTTER: int = Field(default=2, ge=-1, le=4)
93
93
 
94
94
 
95
95
  class UnmappedZebraException(Exception):
@@ -12,10 +12,7 @@ __all__ = [
12
12
  "XrcResult",
13
13
  "ZocaloTrigger",
14
14
  "get_full_processing_results",
15
- "ZOCALO_READING_PLAN_NAME",
16
15
  "NoResultsFromZocalo",
17
16
  "NoZocaloSubscription",
18
17
  "ZocaloStartInfo",
19
18
  ]
20
-
21
- ZOCALO_READING_PLAN_NAME = "zocalo reading"
@@ -1,7 +1,6 @@
1
1
  import asyncio
2
2
  from collections.abc import Generator, Sequence
3
3
  from enum import Enum
4
- from inspect import get_annotations
5
4
  from queue import Empty, Queue
6
5
  from typing import Any, TypedDict
7
6
 
@@ -47,7 +46,6 @@ class ZocaloSource(str, Enum):
47
46
 
48
47
  DEFAULT_TIMEOUT = 180
49
48
  DEFAULT_SORT_KEY = SortKeys.max_count
50
- ZOCALO_READING_PLAN_NAME = "zocalo reading"
51
49
  CLEAR_QUEUE_WAIT_S = 2.0
52
50
  ZOCALO_STAGE_GROUP = "clear zocalo queue"
53
51
 
@@ -389,12 +387,12 @@ def get_full_processing_results(
389
387
  """A plan that will return the raw zocalo results, ranked in descending order according to the sort key.
390
388
  Returns empty list in the event no results found."""
391
389
  LOGGER.info("Retrieving raw zocalo processing results")
392
- com = yield from bps.rd(zocalo.centre_of_mass, default_value=[]) # type: ignore
393
- max_voxel = yield from bps.rd(zocalo.max_voxel, default_value=[]) # type: ignore
394
- max_count = yield from bps.rd(zocalo.max_count, default_value=[]) # type: ignore
395
- n_voxels = yield from bps.rd(zocalo.n_voxels, default_value=[]) # type: ignore
396
- total_count = yield from bps.rd(zocalo.total_count, default_value=[]) # type: ignore
397
- bounding_box = yield from bps.rd(zocalo.bounding_box, default_value=[]) # type: ignore
390
+ com = yield from bps.rd(zocalo.centre_of_mass, default_value=[])
391
+ max_voxel = yield from bps.rd(zocalo.max_voxel, default_value=[])
392
+ max_count = yield from bps.rd(zocalo.max_count, default_value=[])
393
+ n_voxels = yield from bps.rd(zocalo.n_voxels, default_value=[])
394
+ total_count = yield from bps.rd(zocalo.total_count, default_value=[])
395
+ bounding_box = yield from bps.rd(zocalo.bounding_box, default_value=[])
398
396
  return [
399
397
  _corrected_xrc_result(
400
398
  XrcResult(
@@ -410,27 +408,3 @@ def get_full_processing_results(
410
408
  com, max_voxel, max_count, n_voxels, total_count, bounding_box, strict=True
411
409
  )
412
410
  ]
413
-
414
-
415
- def get_processing_results_from_event(
416
- device_name: str, doc: dict
417
- ) -> Sequence[XrcResult]:
418
- """
419
- Decode an event document into the corresponding x-ray centring results
420
-
421
- Args:
422
- doc A bluesky event document containing the signals read from the ZocaloResults
423
- device_name The device name prefix to prepend to the document keys
424
-
425
- Returns:
426
- The list of XrcResults decoded from the event document
427
- """
428
- results_keys = get_annotations(XrcResult).keys()
429
- results_dict = {k: doc["data"][f"{device_name}-{k}"] for k in results_keys}
430
- results_values = [results_dict[k].tolist() for k in results_keys]
431
-
432
- def create_result(*argv):
433
- kwargs = dict(zip(results_keys, argv, strict=False))
434
- return XrcResult(**kwargs)
435
-
436
- return list(map(create_result, *results_values))