dls-dodal 1.42.0__py3-none-any.whl → 1.44.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 (65) hide show
  1. {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/METADATA +5 -4
  2. {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/RECORD +62 -54
  3. {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/b01_1.py +8 -0
  6. dodal/beamlines/i03.py +11 -0
  7. dodal/beamlines/i13_1.py +22 -48
  8. dodal/beamlines/i19_1.py +16 -5
  9. dodal/beamlines/i19_2.py +12 -3
  10. dodal/beamlines/i19_optics.py +4 -2
  11. dodal/beamlines/training_rig.py +5 -1
  12. dodal/common/beamlines/beamline_utils.py +6 -9
  13. dodal/common/data_util.py +20 -0
  14. dodal/common/signal_utils.py +43 -4
  15. dodal/common/visit.py +41 -1
  16. dodal/devices/aperturescatterguard.py +4 -4
  17. dodal/devices/apple2_undulator.py +10 -8
  18. dodal/devices/attenuator/attenuator.py +1 -1
  19. dodal/devices/backlight.py +1 -1
  20. dodal/devices/baton.py +17 -0
  21. dodal/devices/bimorph_mirror.py +2 -2
  22. dodal/devices/current_amplifiers/current_amplifier.py +1 -6
  23. dodal/devices/current_amplifiers/current_amplifier_detector.py +2 -2
  24. dodal/devices/current_amplifiers/femto.py +0 -5
  25. dodal/devices/current_amplifiers/sr570.py +0 -5
  26. dodal/devices/electron_analyser/__init__.py +0 -0
  27. dodal/devices/electron_analyser/base_region.py +64 -0
  28. dodal/devices/electron_analyser/specs/__init__.py +0 -0
  29. dodal/devices/electron_analyser/specs/specs_region.py +24 -0
  30. dodal/devices/electron_analyser/vgscienta/__init__.py +0 -0
  31. dodal/devices/electron_analyser/vgscienta/vgscienta_region.py +77 -0
  32. dodal/devices/fast_grid_scan.py +2 -2
  33. dodal/devices/hutch_shutter.py +1 -1
  34. dodal/devices/i03/beamstop.py +2 -2
  35. dodal/devices/i10/i10_apple2.py +3 -3
  36. dodal/devices/i13_1/merlin.py +1 -2
  37. dodal/devices/i13_1/merlin_controller.py +12 -8
  38. dodal/devices/i19/beamstop.py +30 -0
  39. dodal/devices/i19/hutch_access.py +2 -0
  40. dodal/devices/i19/shutter.py +52 -30
  41. dodal/devices/i22/nxsas.py +1 -3
  42. dodal/devices/i24/focus_mirrors.py +3 -3
  43. dodal/devices/i24/pilatus_metadata.py +2 -2
  44. dodal/devices/i24/pmac.py +2 -2
  45. dodal/devices/oav/oav_detector.py +8 -10
  46. dodal/devices/oav/snapshots/snapshot.py +21 -0
  47. dodal/devices/oav/snapshots/snapshot_image_processing.py +74 -0
  48. dodal/devices/pressure_jump_cell.py +7 -3
  49. dodal/devices/robot.py +1 -1
  50. dodal/devices/thawer.py +4 -4
  51. dodal/devices/undulator.py +1 -1
  52. dodal/devices/undulator_dcm.py +1 -1
  53. dodal/devices/util/epics_util.py +1 -1
  54. dodal/devices/zebra/zebra.py +4 -3
  55. dodal/devices/zebra/zebra_controlled_shutter.py +1 -1
  56. dodal/plan_stubs/data_session.py +17 -9
  57. dodal/plan_stubs/motor_utils.py +10 -12
  58. dodal/plan_stubs/wrapped.py +10 -12
  59. dodal/utils.py +0 -7
  60. dodal/devices/i13_1/merlin_io.py +0 -17
  61. dodal/devices/oav/microns_for_zoom_levels.json +0 -55
  62. dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +0 -64
  63. {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/entry_points.txt +0 -0
  64. {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info/licenses}/LICENSE +0 -0
  65. {dls_dodal-1.42.0.dist-info → dls_dodal-1.44.0.dist-info}/top_level.txt +0 -0
@@ -3,7 +3,6 @@ from ophyd_async.epics import adcore
3
3
 
4
4
  from dodal.common.beamlines.device_helpers import CAM_SUFFIX, HDF5_SUFFIX
5
5
  from dodal.devices.i13_1.merlin_controller import MerlinController
6
- from dodal.devices.i13_1.merlin_io import MerlinDriverIO
7
6
 
8
7
 
9
8
  class Merlin(StandardDetector):
@@ -18,7 +17,7 @@ class Merlin(StandardDetector):
18
17
  fileio_suffix=HDF5_SUFFIX,
19
18
  name: str = "",
20
19
  ):
21
- self.drv = MerlinDriverIO(prefix + drv_suffix)
20
+ self.drv = adcore.ADBaseIO(prefix + drv_suffix)
22
21
  self.hdf = adcore.NDFileHDFIO(prefix + fileio_suffix)
23
22
 
24
23
  super().__init__(
@@ -6,17 +6,21 @@ from ophyd_async.core import (
6
6
  AsyncStatus,
7
7
  TriggerInfo,
8
8
  )
9
- from ophyd_async.epics import adcore
10
- from ophyd_async.epics.adcore import ADBaseController
11
-
12
- from dodal.devices.i13_1.merlin_io import MerlinDriverIO, MerlinImageMode
9
+ from ophyd_async.epics.adcore import (
10
+ DEFAULT_GOOD_STATES,
11
+ ADBaseController,
12
+ ADBaseIO,
13
+ ADImageMode,
14
+ ADState,
15
+ stop_busy_record,
16
+ )
13
17
 
14
18
 
15
19
  class MerlinController(ADBaseController):
16
20
  def __init__(
17
21
  self,
18
- driver: MerlinDriverIO,
19
- good_states: frozenset[adcore.DetectorState] = adcore.DEFAULT_GOOD_STATES,
22
+ driver: ADBaseIO,
23
+ good_states: frozenset[ADState] = DEFAULT_GOOD_STATES,
20
24
  ) -> None:
21
25
  self.driver = driver
22
26
  self.good_states = good_states
@@ -34,7 +38,7 @@ class MerlinController(ADBaseController):
34
38
  )
35
39
  await asyncio.gather(
36
40
  self.driver.num_images.set(trigger_info.total_number_of_triggers),
37
- self.driver.image_mode.set(MerlinImageMode.MULTIPLE),
41
+ self.driver.image_mode.set(ADImageMode.MULTIPLE),
38
42
  )
39
43
 
40
44
  async def wait_for_idle(self):
@@ -44,4 +48,4 @@ class MerlinController(ADBaseController):
44
48
  async def disarm(self):
45
49
  # We can't use caput callback as we already used it in arm() and we can't have
46
50
  # 2 or they will deadlock
47
- await adcore.stop_busy_record(self.driver.acquire, False, timeout=1)
51
+ await stop_busy_record(self.driver.acquire, False, timeout=1)
@@ -0,0 +1,30 @@
1
+ from ophyd_async.core import StandardReadable, StrictEnum
2
+ from ophyd_async.epics.core import epics_signal_rw, epics_signal_x
3
+ from ophyd_async.epics.motor import Motor
4
+
5
+
6
+ class HomeGroup(StrictEnum):
7
+ NONE = "none"
8
+ ALL = "All"
9
+ X = "X"
10
+ Y = "Y"
11
+ Z = "Z"
12
+
13
+
14
+ class HomingControl(StandardReadable):
15
+ def __init__(self, prefix: str, name: str = "") -> None:
16
+ self.homing_group = epics_signal_rw(HomeGroup, f"{prefix}:HMGRP")
17
+ self.home = epics_signal_x(f"{prefix}:HOME")
18
+ super().__init__(name)
19
+
20
+
21
+ class BeamStop(StandardReadable):
22
+ def __init__(self, prefix: str, name: str = "") -> None:
23
+ with self.add_children_as_readables():
24
+ self.x = Motor(f"{prefix}X")
25
+ self.y = Motor(f"{prefix}Y")
26
+ self.z = Motor(f"{prefix}Z")
27
+
28
+ self.homing = HomingControl(f"{prefix}HM", name)
29
+
30
+ super().__init__(name)
@@ -1,6 +1,8 @@
1
1
  from ophyd_async.core import StandardReadable
2
2
  from ophyd_async.epics.core import epics_signal_r
3
3
 
4
+ ACCESS_DEVICE_NAME = "access_control" # Device name in i19-blueapi
5
+
4
6
 
5
7
  class HutchAccessControl(StandardReadable):
6
8
  def __init__(self, prefix: str, name: str = "") -> None:
@@ -1,57 +1,79 @@
1
1
  from enum import Enum
2
2
 
3
+ from aiohttp import ClientSession
3
4
  from bluesky.protocols import Movable
4
- from ophyd_async.core import AsyncStatus, StandardReadable
5
+ from ophyd_async.core import AsyncStatus, StandardReadable, StandardReadableFormat
5
6
  from ophyd_async.epics.core import epics_signal_r
6
7
 
7
- from dodal.devices.hutch_shutter import HutchShutter, ShutterDemand
8
+ from dodal.devices.hutch_shutter import ShutterDemand, ShutterState
9
+ from dodal.devices.i19.hutch_access import ACCESS_DEVICE_NAME
8
10
  from dodal.log import LOGGER
9
11
 
10
-
11
- class HutchInvalidError(Exception):
12
- pass
12
+ OPTICS_BLUEAPI_URL = "https://i19-blueapi.diamond.ac.uk"
13
13
 
14
14
 
15
15
  class HutchState(str, Enum):
16
16
  EH1 = "EH1"
17
17
  EH2 = "EH2"
18
- INVALID = "INVALID"
19
18
 
20
19
 
21
- class HutchConditionalShutter(StandardReadable, Movable):
20
+ class AccessControlledShutter(StandardReadable, Movable[ShutterDemand]):
22
21
  """ I19-specific device to operate the hutch shutter.
23
22
 
24
- This device evaluates the hutch state value to work out which of the two I19 \
25
- hutches is in use and then implements the HutchShutter device to operate the \
26
- experimental shutter.
23
+ This device will send a REST call to the blueapi instance controlling the optics \
24
+ hutch running on the I19 cluster, which will evaluate the current hutch in use vs \
25
+ the hutch sending the request and decide if the plan will be run or not.
27
26
  As the two hutches are located in series, checking the hutch in use is necessary to \
28
27
  avoid accidentally operating the shutter from one hutch while the other has beamtime.
29
28
 
30
- The hutch name should be passed to the device upon instantiation. If this does not \
31
- coincide with the current hutch in use, a warning will be logged and the shutter \
32
- will not be operated. This is to allow for testing of plans.
33
- An error will instead be raised if the hutch state reads as "INVALID".
29
+ The name of the hutch that wants to operate the shutter should be passed to the \
30
+ device upon instantiation.
31
+
32
+ For details see the architecture described in \
33
+ https://github.com/DiamondLightSource/i19-bluesky/issues/30.
34
34
  """
35
35
 
36
36
  def __init__(self, prefix: str, hutch: HutchState, name: str = "") -> None:
37
- self.shutter = HutchShutter(prefix=prefix, name=name)
38
- bl_prefix = prefix.split("-")[0]
39
- self.hutch_state = epics_signal_r(str, f"{bl_prefix}-OP-STAT-01:EHStatus.VALA")
37
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
38
+ self.shutter_status = epics_signal_r(ShutterState, f"{prefix}STA")
40
39
  self.hutch_request = hutch
40
+ self.url = OPTICS_BLUEAPI_URL
41
41
  super().__init__(name)
42
42
 
43
43
  @AsyncStatus.wrap
44
44
  async def set(self, value: ShutterDemand):
45
- hutch_in_use = await self.hutch_state.get_value()
46
- LOGGER.info(f"Current hutch in use: {hutch_in_use}")
47
- if hutch_in_use == HutchState.INVALID:
48
- raise HutchInvalidError(
49
- "The hutch state is invalid. Contact the beamline staff."
50
- )
51
- if hutch_in_use != self.hutch_request:
52
- # NOTE Warn but don't fail
53
- LOGGER.warning(
54
- f"{self.hutch_request} is not the hutch in use. Shutter will not be operated."
55
- )
56
- else:
57
- await self.shutter.set(value)
45
+ REQUEST_PARAMS = {
46
+ "name": "operate_shutter_plan",
47
+ "params": {
48
+ "experiment_hutch": self.hutch_request.value,
49
+ "access_device": ACCESS_DEVICE_NAME,
50
+ "shutter_demand": value,
51
+ },
52
+ }
53
+ async with ClientSession(base_url=self.url, raise_for_status=True) as session:
54
+ # First submit the plan to the worker
55
+ async with session.post("/tasks", data=REQUEST_PARAMS) as response:
56
+ LOGGER.debug(
57
+ f"Task submitted to the worker, response status: {response.status}"
58
+ )
59
+
60
+ try:
61
+ data = await response.json()
62
+ task_id = data["task_id"]
63
+ except Exception as e:
64
+ LOGGER.error(
65
+ f"Failed to get task_id from {self.url}/tasks POST. ({e})"
66
+ )
67
+ raise
68
+ # Then set the task as active and run asap
69
+ async with session.put(
70
+ "/worker/tasks", data={"task_id": task_id}
71
+ ) as response:
72
+ if not response.ok:
73
+ LOGGER.error(
74
+ f"""Unable to operate the shutter.
75
+ Session PUT responded with {response.status}: {response.reason}.
76
+ """
77
+ )
78
+ return
79
+ LOGGER.debug(f"Run operate shutter plan, task_id: {task_id}")
@@ -6,7 +6,7 @@ from typing import TypeVar
6
6
  from bluesky.protocols import Reading
7
7
  from event_model.documents.event_descriptor import DataKey
8
8
  from ophyd_async.core import PathProvider
9
- from ophyd_async.epics.adaravis import AravisController, AravisDetector
9
+ from ophyd_async.epics.adaravis import AravisDetector
10
10
  from ophyd_async.epics.adpilatus import PilatusDetector
11
11
 
12
12
  ValueAndUnits = tuple[float, str]
@@ -149,7 +149,6 @@ class NXSasOAV(AravisDetector):
149
149
  fileio_suffix: str,
150
150
  metadata_holder: NXSasMetadataHolder,
151
151
  name: str = "",
152
- gpio_number: AravisController.GPIO_NUMBER = 1,
153
152
  ):
154
153
  """Extends detector with configuration metadata required or desired
155
154
  to comply with the NXsas application definition.
@@ -162,7 +161,6 @@ class NXSasOAV(AravisDetector):
162
161
  drv_suffix=drv_suffix,
163
162
  fileio_suffix=fileio_suffix,
164
163
  name=name,
165
- gpio_number=gpio_number,
166
164
  )
167
165
  self._metadata_holder = metadata_holder
168
166
 
@@ -1,7 +1,7 @@
1
1
  from ophyd_async.core import StandardReadable, StrictEnum
2
2
  from ophyd_async.epics.core import epics_signal_rw
3
3
 
4
- from dodal.common.signal_utils import create_hardware_backed_soft_signal
4
+ from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
5
5
 
6
6
 
7
7
  class HFocusMode(StrictEnum):
@@ -40,10 +40,10 @@ class FocusMirrorsMode(StandardReadable):
40
40
  self.vertical = epics_signal_rw(VFocusMode, prefix + "G0:TARGETAPPLY")
41
41
 
42
42
  with self.add_children_as_readables():
43
- self.beam_size_x = create_hardware_backed_soft_signal(
43
+ self.beam_size_x = create_r_hardware_backed_soft_signal(
44
44
  int, self._get_beam_size_x, units="um"
45
45
  )
46
- self.beam_size_y = create_hardware_backed_soft_signal(
46
+ self.beam_size_y = create_r_hardware_backed_soft_signal(
47
47
  int, self._get_beam_size_y, units="um"
48
48
  )
49
49
 
@@ -5,7 +5,7 @@ import re
5
5
  from ophyd_async.core import StandardReadable
6
6
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
7
7
 
8
- from dodal.common.signal_utils import create_hardware_backed_soft_signal
8
+ from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
9
9
 
10
10
 
11
11
  class PilatusMetadata(StandardReadable):
@@ -14,7 +14,7 @@ class PilatusMetadata(StandardReadable):
14
14
  self.template = epics_signal_r(str, prefix + "cam1:FileTemplate_RBV")
15
15
  self.filenumber = epics_signal_r(int, prefix + "cam1:FileNumber_RBV")
16
16
  with self.add_children_as_readables():
17
- self.filename_template = create_hardware_backed_soft_signal(
17
+ self.filename_template = create_r_hardware_backed_soft_signal(
18
18
  str, self._get_full_filename_template
19
19
  )
20
20
  super().__init__(name)
dodal/devices/i24/pmac.py CHANGED
@@ -71,7 +71,7 @@ class PMACStringMove(Triggerable):
71
71
  await self.signal_ref().set(self.cmd_string, wait=True)
72
72
 
73
73
 
74
- class PMACStringLaser(Device, Movable):
74
+ class PMACStringLaser(Device, Movable[LaserSettings]):
75
75
  """Set the pmac_string to control the laser."""
76
76
 
77
77
  def __init__(
@@ -90,7 +90,7 @@ class PMACStringLaser(Device, Movable):
90
90
  await self._signal_ref().set(value.value)
91
91
 
92
92
 
93
- class PMACStringEncReset(Device, Movable):
93
+ class PMACStringEncReset(Device, Movable[EncReset]):
94
94
  """Set a pmac_string to control the encoder channels in the controller."""
95
95
 
96
96
  def __init__(
@@ -9,10 +9,10 @@ from ophyd_async.core import (
9
9
  )
10
10
  from ophyd_async.epics.core import epics_signal_rw
11
11
 
12
- from dodal.common.signal_utils import create_hardware_backed_soft_signal
12
+ from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
13
13
  from dodal.devices.areadetector.plugins.CAM import Cam
14
14
  from dodal.devices.oav.oav_parameters import DEFAULT_OAV_WINDOW, OAVConfig
15
- from dodal.devices.oav.snapshots.snapshot_with_beam_centre import SnapshotWithBeamCentre
15
+ from dodal.devices.oav.snapshots.snapshot import Snapshot
16
16
  from dodal.devices.oav.snapshots.snapshot_with_grid import SnapshotWithGrid
17
17
 
18
18
 
@@ -29,7 +29,7 @@ def _get_correct_zoom_string(zoom: str) -> str:
29
29
  return zoom
30
30
 
31
31
 
32
- class ZoomController(StandardReadable, Movable):
32
+ class ZoomController(StandardReadable, Movable[str]):
33
33
  """
34
34
  Device to control the zoom level. This should be set like
35
35
  o = OAV(name="oav")
@@ -63,24 +63,22 @@ class OAV(StandardReadable):
63
63
 
64
64
  with self.add_children_as_readables():
65
65
  self.grid_snapshot = SnapshotWithGrid(f"{prefix}MJPG:", name)
66
- self.microns_per_pixel_x = create_hardware_backed_soft_signal(
66
+ self.microns_per_pixel_x = create_r_hardware_backed_soft_signal(
67
67
  float,
68
68
  lambda: self._get_microns_per_pixel(Coords.X),
69
69
  )
70
- self.microns_per_pixel_y = create_hardware_backed_soft_signal(
70
+ self.microns_per_pixel_y = create_r_hardware_backed_soft_signal(
71
71
  float,
72
72
  lambda: self._get_microns_per_pixel(Coords.Y),
73
73
  )
74
- self.beam_centre_i = create_hardware_backed_soft_signal(
74
+ self.beam_centre_i = create_r_hardware_backed_soft_signal(
75
75
  int, lambda: self._get_beam_position(Coords.X)
76
76
  )
77
- self.beam_centre_j = create_hardware_backed_soft_signal(
77
+ self.beam_centre_j = create_r_hardware_backed_soft_signal(
78
78
  int, lambda: self._get_beam_position(Coords.Y)
79
79
  )
80
- self.snapshot = SnapshotWithBeamCentre(
80
+ self.snapshot = Snapshot(
81
81
  f"{self._prefix}MJPG:",
82
- self.beam_centre_i,
83
- self.beam_centre_j,
84
82
  self._name,
85
83
  )
86
84
 
@@ -0,0 +1,21 @@
1
+ from PIL import Image
2
+
3
+ from dodal.devices.areadetector.plugins.MJPG import MJPG
4
+
5
+ CROSSHAIR_LENGTH_PX = 20
6
+ CROSSHAIR_OUTLINE_COLOUR = "Black"
7
+ CROSSHAIR_FILL_COLOUR = "White"
8
+
9
+
10
+ class Snapshot(MJPG):
11
+ """A child of MJPG which, when triggered, saves the image to disk."""
12
+
13
+ def __init__(
14
+ self,
15
+ prefix: str,
16
+ name: str = "",
17
+ ) -> None:
18
+ super().__init__(prefix, name)
19
+
20
+ async def post_processing(self, image: Image.Image):
21
+ await self._save_image(image)
@@ -0,0 +1,74 @@
1
+ from PIL import Image, ImageDraw
2
+
3
+ from dodal.devices.oav.utils import Pixel
4
+
5
+ CROSSHAIR_LENGTH_PX = 20
6
+ CROSSHAIR_OUTLINE_COLOUR = "Black"
7
+ CROSSHAIR_FILL_COLOUR = "White"
8
+
9
+
10
+ def draw_crosshair(image: Image.Image, beam_x: int, beam_y: int):
11
+ """
12
+ Draw a crosshair at the beam centre coordinates specified.
13
+ Args:
14
+ image: The image to draw the crosshair onto. This is mutated.
15
+ beam_x: The x-coordinate of the crosshair (pixels)
16
+ beam_y: The y-coordinate of the crosshair (pixels)
17
+ """
18
+ draw = ImageDraw.Draw(image)
19
+ OUTLINE_WIDTH = 1
20
+ HALF_LEN = CROSSHAIR_LENGTH_PX / 2
21
+ draw.rectangle(
22
+ [
23
+ beam_x - OUTLINE_WIDTH,
24
+ beam_y - HALF_LEN - OUTLINE_WIDTH,
25
+ beam_x + OUTLINE_WIDTH,
26
+ beam_y + HALF_LEN + OUTLINE_WIDTH,
27
+ ],
28
+ fill=CROSSHAIR_OUTLINE_COLOUR,
29
+ )
30
+ draw.rectangle(
31
+ [
32
+ beam_x - HALF_LEN - OUTLINE_WIDTH,
33
+ beam_y - OUTLINE_WIDTH,
34
+ beam_x + HALF_LEN + OUTLINE_WIDTH,
35
+ beam_y + OUTLINE_WIDTH,
36
+ ],
37
+ fill=CROSSHAIR_OUTLINE_COLOUR,
38
+ )
39
+ draw.line(
40
+ ((beam_x, beam_y - HALF_LEN), (beam_x, beam_y + HALF_LEN)),
41
+ fill=CROSSHAIR_FILL_COLOUR,
42
+ )
43
+ draw.line(
44
+ ((beam_x - HALF_LEN, beam_y), (beam_x + HALF_LEN, beam_y)),
45
+ fill=CROSSHAIR_FILL_COLOUR,
46
+ )
47
+
48
+
49
+ def compute_beam_centre_pixel_xy_for_mm_position(
50
+ sample_pos_mm: tuple[float, float],
51
+ beam_pos_at_origin_px: Pixel,
52
+ microns_per_pixel: tuple[float, float],
53
+ ) -> Pixel:
54
+ """
55
+ Compute the location of the beam centre in pixels on a reference image.
56
+ Args:
57
+ sample_pos_mm: x, y location of the sample in mm relative to when the reference image
58
+ was taken.
59
+ beam_pos_at_origin_px: x, y position of the beam centre in the reference image (pixels)
60
+ microns_per_pixel: x, y scaling factor relating the sample position to the position in the image.
61
+ Returns:
62
+ x, y location of the beam centre (pixels)
63
+
64
+ """
65
+
66
+ def centre(sample_pos, beam_pos, um_per_px) -> int:
67
+ return beam_pos + sample_pos * 1000 / um_per_px
68
+
69
+ return Pixel(
70
+ centre(sp, bp, mpp)
71
+ for sp, bp, mpp in zip(
72
+ sample_pos_mm, beam_pos_at_origin_px, microns_per_pixel, strict=True
73
+ )
74
+ )
@@ -76,7 +76,7 @@ class AllValvesControlState:
76
76
  valve_6: FastValveControlRequest | None = None
77
77
 
78
78
 
79
- class AllValvesControl(StandardReadable, Movable):
79
+ class AllValvesControl(StandardReadable, Movable[AllValvesControlState]):
80
80
  """
81
81
  valves 2, 4, 7, 8 are not controlled by the IOC,
82
82
  as they are under manual control.
@@ -151,7 +151,9 @@ class AllValvesControl(StandardReadable, Movable):
151
151
  )
152
152
 
153
153
 
154
- class ValveControl(StandardReadable, Movable):
154
+ class ValveControl(
155
+ StandardReadable, Movable[ValveControlRequest | ValveOpenSeqRequest]
156
+ ):
155
157
  def __init__(self, prefix: str, name: str = "") -> None:
156
158
  with self.add_children_as_readables():
157
159
  self.close = epics_signal_rw(ValveControlRequest, prefix + ":CON")
@@ -170,7 +172,9 @@ class ValveControl(StandardReadable, Movable):
170
172
  return set_status
171
173
 
172
174
 
173
- class FastValveControl(StandardReadable, Movable):
175
+ class FastValveControl(
176
+ StandardReadable, Movable[FastValveControlRequest | ValveOpenSeqRequest]
177
+ ):
174
178
  def __init__(self, prefix: str, name: str = "") -> None:
175
179
  with self.add_children_as_readables():
176
180
  self.close = epics_signal_rw(FastValveControlRequest, prefix + ":CON")
dodal/devices/robot.py CHANGED
@@ -52,7 +52,7 @@ class ErrorStatus(Device):
52
52
  raise RobotLoadFailed(int(error_code), error_string) from raise_from
53
53
 
54
54
 
55
- class BartRobot(StandardReadable, Movable):
55
+ class BartRobot(StandardReadable, Movable[SampleLocation]):
56
56
  """The sample changing robot."""
57
57
 
58
58
  # How long to wait for the robot if it is busy soaking/drying
dodal/devices/thawer.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from asyncio import Task, create_task, sleep
2
2
 
3
- from bluesky.protocols import Stoppable
3
+ from bluesky.protocols import Movable, Stoppable
4
4
  from ophyd_async.core import (
5
5
  AsyncStatus,
6
6
  Device,
@@ -21,18 +21,18 @@ class ThawerStates(StrictEnum):
21
21
  ON = "On"
22
22
 
23
23
 
24
- class ThawingTimer(Device, Stoppable):
24
+ class ThawingTimer(Device, Stoppable, Movable[float]):
25
25
  def __init__(self, control_signal: SignalRW[ThawerStates]) -> None:
26
26
  self._control_signal_ref = Reference(control_signal)
27
27
  self._thawing_task: Task | None = None
28
28
  super().__init__("thaw_for_time_s")
29
29
 
30
30
  @AsyncStatus.wrap
31
- async def set(self, time_to_thaw_for: float):
31
+ async def set(self, value: float):
32
32
  await self._control_signal_ref().set(ThawerStates.ON)
33
33
  if self._thawing_task and not self._thawing_task.done():
34
34
  raise ThawingException("Thawing task already in progress")
35
- self._thawing_task = create_task(sleep(time_to_thaw_for))
35
+ self._thawing_task = create_task(sleep(value))
36
36
  try:
37
37
  await self._thawing_task
38
38
  finally:
@@ -46,7 +46,7 @@ def _get_closest_gap_for_energy(
46
46
  return table[1][idx]
47
47
 
48
48
 
49
- class Undulator(StandardReadable, Movable):
49
+ class Undulator(StandardReadable, Movable[float]):
50
50
  """
51
51
  An Undulator-type insertion device, used to control photon emission at a given
52
52
  beam energy.
@@ -12,7 +12,7 @@ from .undulator import Undulator
12
12
  ENERGY_TIMEOUT_S: float = 30.0
13
13
 
14
14
 
15
- class UndulatorDCM(StandardReadable, Movable):
15
+ class UndulatorDCM(StandardReadable, Movable[float]):
16
16
  """
17
17
  Composite device to handle changing beamline energies, wraps the Undulator and the
18
18
  DCM. The DCM has a motor which controls the beam energy, when it moves, the
@@ -114,7 +114,7 @@ def call_func(func: Callable[[], StatusBase]) -> StatusBase:
114
114
  return func()
115
115
 
116
116
 
117
- class SetWhenEnabled(OphydAsyncDevice, Movable):
117
+ class SetWhenEnabled(OphydAsyncDevice, Movable[int]):
118
118
  """A device that sets the proc field of a PV when it becomes enabled."""
119
119
 
120
120
  def __init__(self, name: str = "", prefix: str = ""):
@@ -4,6 +4,7 @@ import asyncio
4
4
  from enum import Enum
5
5
  from functools import partialmethod
6
6
 
7
+ from bluesky.protocols import Movable
7
8
  from ophyd_async.core import (
8
9
  AsyncStatus,
9
10
  DeviceVector,
@@ -74,7 +75,7 @@ class SoftInState(StrictEnum):
74
75
  NO = "No"
75
76
 
76
77
 
77
- class ArmingDevice(StandardReadable):
78
+ class ArmingDevice(StandardReadable, Movable[ArmDemand]):
78
79
  """A useful device that can abstract some of the logic of arming.
79
80
  Allows a user to just call arm.set(ArmDemand.ARM)"""
80
81
 
@@ -94,8 +95,8 @@ class ArmingDevice(StandardReadable):
94
95
  return
95
96
 
96
97
  @AsyncStatus.wrap
97
- async def set(self, demand: ArmDemand):
98
- await asyncio.wait_for(self._set_armed(demand), timeout=self.TIMEOUT)
98
+ async def set(self, value: ArmDemand):
99
+ await asyncio.wait_for(self._set_armed(value), timeout=self.TIMEOUT)
99
100
 
100
101
 
101
102
  class PositionCompare(StandardReadable):
@@ -19,7 +19,7 @@ class ZebraShutterControl(StrictEnum):
19
19
  AUTO = "Auto"
20
20
 
21
21
 
22
- class ZebraShutter(StandardReadable, Movable):
22
+ class ZebraShutter(StandardReadable, Movable[ZebraShutterState]):
23
23
  """The shutter on most MX beamlines is controlled by the zebra.
24
24
 
25
25
  Internally in the zebra there are two AND gates, one for manual control and one for
@@ -1,6 +1,9 @@
1
+ import logging
2
+
1
3
  from bluesky import plan_stubs as bps
2
4
  from bluesky import preprocessors as bpp
3
5
  from bluesky.utils import MsgGenerator, make_decorator
6
+ from ophyd_async.core import PathProvider
4
7
 
5
8
  from dodal.common.beamlines.beamline_utils import get_path_provider
6
9
  from dodal.common.types import UpdatingPathProvider
@@ -10,7 +13,7 @@ DATA_GROUPS = "data_groups"
10
13
 
11
14
 
12
15
  def attach_data_session_metadata_wrapper(
13
- plan: MsgGenerator, provider: UpdatingPathProvider | None = None
16
+ plan: MsgGenerator, provider: PathProvider | None = None
14
17
  ) -> MsgGenerator:
15
18
  """
16
19
  Attach data session metadata to the runs within a plan and make it correlate
@@ -30,14 +33,19 @@ def attach_data_session_metadata_wrapper(
30
33
  Yields:
31
34
  Iterator[Msg]: Plan messages
32
35
  """
33
- if provider is None:
34
- provider = get_path_provider()
35
- yield from bps.wait_for([provider.update])
36
- ress = yield from bps.wait_for([provider.data_session])
37
- data_session = ress[0].result()
38
- # https://github.com/DiamondLightSource/dodal/issues/452
39
- # As part of 452, write each dataCollection into their own folder, then can use resource_dir directly
40
- yield from bpp.inject_md_wrapper(plan, md={DATA_SESSION: data_session})
36
+ provider = provider or get_path_provider()
37
+ if isinstance(provider, UpdatingPathProvider):
38
+ yield from bps.wait_for([provider.update])
39
+ ress = yield from bps.wait_for([provider.data_session])
40
+ data_session = ress[0].result()
41
+ # https://github.com/DiamondLightSource/dodal/issues/452
42
+ # As part of 452, write each dataCollection into their own folder, then can use resource_dir directly
43
+ yield from bpp.inject_md_wrapper(plan, md={DATA_SESSION: data_session})
44
+ else:
45
+ logging.warning(
46
+ f"{provider} is not an UpdatingPathProvider, {attach_data_session_metadata_wrapper.__name__} will have no effect"
47
+ )
48
+ yield from plan
41
49
 
42
50
 
43
51
  attach_data_session_metadata_decorator = make_decorator(