dls-dodal 1.43.0__py3-none-any.whl → 1.45.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 (70) hide show
  1. {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/METADATA +4 -3
  2. {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/RECORD +66 -49
  3. {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/__init__.py +2 -0
  6. dodal/beamlines/b01_1.py +8 -0
  7. dodal/beamlines/b07.py +27 -0
  8. dodal/beamlines/b07_1.py +25 -0
  9. dodal/beamlines/i03.py +11 -0
  10. dodal/beamlines/i09.py +25 -0
  11. dodal/beamlines/i09_1.py +25 -0
  12. dodal/beamlines/i10.py +19 -35
  13. dodal/beamlines/i13_1.py +22 -48
  14. dodal/beamlines/i19_1.py +17 -5
  15. dodal/beamlines/i19_2.py +13 -3
  16. dodal/beamlines/i19_optics.py +4 -2
  17. dodal/beamlines/i20_1.py +2 -1
  18. dodal/beamlines/i23.py +10 -0
  19. dodal/beamlines/p60.py +21 -0
  20. dodal/common/data_util.py +20 -0
  21. dodal/common/signal_utils.py +43 -4
  22. dodal/common/visit.py +1 -41
  23. dodal/devices/aperturescatterguard.py +3 -3
  24. dodal/devices/baton.py +17 -0
  25. dodal/devices/current_amplifiers/current_amplifier.py +1 -6
  26. dodal/devices/current_amplifiers/current_amplifier_detector.py +2 -2
  27. dodal/devices/current_amplifiers/femto.py +0 -5
  28. dodal/devices/current_amplifiers/sr570.py +0 -5
  29. dodal/devices/detector/det_dist_to_beam_converter.py +16 -23
  30. dodal/devices/detector/detector.py +2 -1
  31. dodal/devices/electron_analyser/__init__.py +0 -0
  32. dodal/devices/electron_analyser/abstract_analyser_io.py +47 -0
  33. dodal/devices/electron_analyser/abstract_region.py +112 -0
  34. dodal/devices/electron_analyser/specs_analyser_io.py +19 -0
  35. dodal/devices/electron_analyser/specs_region.py +26 -0
  36. dodal/devices/electron_analyser/vgscienta_analyser_io.py +26 -0
  37. dodal/devices/electron_analyser/vgscienta_region.py +90 -0
  38. dodal/devices/fast_grid_scan.py +2 -2
  39. dodal/devices/i03/beamstop.py +2 -2
  40. dodal/devices/i10/diagnostics.py +239 -0
  41. dodal/devices/i10/slits.py +93 -6
  42. dodal/devices/i13_1/merlin.py +1 -2
  43. dodal/devices/i13_1/merlin_controller.py +12 -8
  44. dodal/devices/i19/beamstop.py +30 -0
  45. dodal/devices/i19/blueapi_device.py +102 -0
  46. dodal/devices/i19/hutch_access.py +2 -0
  47. dodal/devices/i19/shutter.py +24 -40
  48. dodal/devices/i22/nxsas.py +1 -3
  49. dodal/devices/i24/focus_mirrors.py +3 -3
  50. dodal/devices/i24/pilatus_metadata.py +2 -2
  51. dodal/devices/motors.py +21 -0
  52. dodal/devices/oav/oav_detector.py +7 -9
  53. dodal/devices/oav/snapshots/snapshot.py +21 -0
  54. dodal/devices/oav/snapshots/snapshot_image_processing.py +74 -0
  55. dodal/devices/turbo_slit.py +8 -2
  56. dodal/devices/undulator.py +9 -7
  57. dodal/devices/util/adjuster_plans.py +1 -2
  58. dodal/devices/util/lookup_tables.py +38 -0
  59. dodal/devices/util/test_utils.py +1 -0
  60. dodal/plan_stubs/electron_analyser/__init__.py +0 -0
  61. dodal/plan_stubs/electron_analyser/configure_controller.py +80 -0
  62. dodal/plan_stubs/motor_utils.py +10 -12
  63. dodal/utils.py +0 -7
  64. dodal/devices/i13_1/merlin_io.py +0 -17
  65. dodal/devices/oav/microns_for_zoom_levels.json +0 -55
  66. dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +0 -64
  67. dodal/devices/util/motor_utils.py +0 -6
  68. {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/entry_points.txt +0 -0
  69. {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info/licenses}/LICENSE +0 -0
  70. {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/top_level.txt +0 -0
@@ -1,15 +1,21 @@
1
+ from ophyd_async.core import Device
2
+ from ophyd_async.core._device import DeviceConnector
3
+ from ophyd_async.epics.core import epics_signal_r
1
4
  from ophyd_async.epics.motor import Motor
2
5
 
3
- from dodal.devices.slits import Slits
6
+ from dodal.devices.slits import MinimalSlits, Slits
4
7
 
5
8
 
6
- class I10Slits(Slits):
9
+ class I10SlitsBlades(Slits):
10
+ """Slits with extra control for each blade."""
11
+
7
12
  def __init__(self, prefix: str, name: str = "") -> None:
8
13
  with self.add_children_as_readables():
9
- self.x_ring_blade = Motor(prefix + "XRING")
10
- self.x_hall_blade = Motor(prefix + "XHALL")
11
- self.y_top_blade = Motor(prefix + "YPLUS")
12
- self.y_bot_blade = Motor(prefix + "YMINUS")
14
+ self.ring_blade = Motor(prefix + "XRING")
15
+ self.hall_blade = Motor(prefix + "XHALL")
16
+ self.top_blade = Motor(prefix + "YPLUS")
17
+ self.bot_blade = Motor(prefix + "YMINUS")
18
+
13
19
  super().__init__(
14
20
  prefix=prefix,
15
21
  x_gap="XSIZE",
@@ -20,7 +26,41 @@ class I10Slits(Slits):
20
26
  )
21
27
 
22
28
 
29
+ class BladeDrainCurrents(Device):
30
+ """ "The drain current measurements on each blade. The drain current are due to
31
+ photoelectric effect (https://en.wikipedia.org/wiki/Photoelectric_effect).
32
+ Note the readings are in voltage as it is the output of a current amplifier."""
33
+
34
+ def __init__(
35
+ self,
36
+ prefix: str,
37
+ suffix_ring_blade: str = "SIG1",
38
+ suffix_hall_blade: str = "SIG2",
39
+ suffix_top_blade: str = "SIG3",
40
+ suffix_bot_blade: str = "SIG4",
41
+ name: str = "",
42
+ connector: DeviceConnector | None = None,
43
+ ) -> None:
44
+ self.ring_blade_current = epics_signal_r(
45
+ float, read_pv=prefix + suffix_ring_blade
46
+ )
47
+ self.hall_blade_current = epics_signal_r(
48
+ float, read_pv=prefix + suffix_hall_blade
49
+ )
50
+ self.top_blade_current = epics_signal_r(
51
+ float, read_pv=prefix + suffix_top_blade
52
+ )
53
+ self.bot_blade_current = epics_signal_r(
54
+ float, read_pv=prefix + suffix_bot_blade
55
+ )
56
+
57
+ super().__init__(name, connector)
58
+
59
+
23
60
  class I10PrimarySlits(Slits):
61
+ """First slits of the beamline with very high power load, they are two square water
62
+ cooled blocks(aperture/aptr) that overlap to produce slit like behavior."""
63
+
24
64
  def __init__(self, prefix: str, name: str = "") -> None:
25
65
  with self.add_children_as_readables():
26
66
  self.x_aptr_1 = Motor(prefix + "APTR1:X")
@@ -35,3 +75,50 @@ class I10PrimarySlits(Slits):
35
75
  y_centre="YCENTRE",
36
76
  name=name,
37
77
  )
78
+
79
+
80
+ class I10Slits(Device):
81
+ """Collection of all the i10 slits before end station."""
82
+
83
+ def __init__(self, prefix: str, name: str = "") -> None:
84
+ self.s1 = I10PrimarySlits(
85
+ prefix=prefix + "01:",
86
+ )
87
+ self.s2 = I10SlitsBlades(
88
+ prefix=prefix + "02:",
89
+ )
90
+ self.s3 = I10SlitsBlades(
91
+ prefix=prefix + "03:",
92
+ )
93
+ self.s4 = MinimalSlits(
94
+ prefix=prefix + "04:",
95
+ x_gap="XSIZE",
96
+ y_gap="YSIZE",
97
+ )
98
+ self.s5 = I10SlitsBlades(
99
+ prefix=prefix + "05:",
100
+ )
101
+ self.s6 = I10SlitsBlades(
102
+ prefix=prefix + "06:",
103
+ )
104
+ super().__init__(name=name)
105
+
106
+
107
+ class I10SlitsDrainCurrent(Device):
108
+ """Collection of all the drain current from i10 slits."""
109
+
110
+ def __init__(
111
+ self, prefix: str, name: str = "", connector: DeviceConnector | None = None
112
+ ) -> None:
113
+ self.s2 = BladeDrainCurrents(
114
+ prefix=prefix + "AL-SLITS-02:",
115
+ suffix_ring_blade="XRING:I",
116
+ suffix_hall_blade="XHALL:I",
117
+ suffix_top_blade="YPLUS:I",
118
+ suffix_bot_blade="YMINUS:I",
119
+ )
120
+ self.s3 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-01:")
121
+ self.s4 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-02:")
122
+ self.s5 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-03:")
123
+ self.s6 = BladeDrainCurrents(prefix=prefix + "DI-IAMP-04:")
124
+ super().__init__(name, connector)
@@ -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)
@@ -0,0 +1,102 @@
1
+ import asyncio
2
+ import json
3
+ from enum import Enum
4
+ from typing import TypeVar
5
+
6
+ from aiohttp import ClientSession
7
+ from bluesky.protocols import Movable
8
+ from ophyd_async.core import AsyncStatus, StandardReadable
9
+
10
+ from dodal.log import LOGGER
11
+
12
+ OPTICS_BLUEAPI_URL = "https://i19-blueapi.diamond.ac.uk"
13
+ HEADERS = {"Accept": "application/json", "Content-Type": "application/json"}
14
+
15
+ D = TypeVar("D")
16
+
17
+
18
+ class HutchState(str, Enum):
19
+ EH1 = "EH1"
20
+ EH2 = "EH2"
21
+
22
+
23
+ class OpticsBlueAPIDevice(StandardReadable, Movable[D]):
24
+ """General device that a REST call to the blueapi instance controlling the optics \
25
+ hutch running on the I19 cluster, which will evaluate the current hutch in use vs \
26
+ the hutch sending the request and decide if the plan will be run or not.
27
+
28
+ For details see the architecture described in \
29
+ https://github.com/DiamondLightSource/i19-bluesky/issues/30.
30
+ """
31
+
32
+ def __init__(self, name: str = "") -> None:
33
+ self.url = OPTICS_BLUEAPI_URL
34
+ self.headers = HEADERS
35
+ super().__init__(name)
36
+
37
+ @AsyncStatus.wrap
38
+ async def set(self, value: D):
39
+ """ On set send a POST request to the optics blueapi with the name and \
40
+ parameters, gets the generated task_id and then sends a PUT request that runs \
41
+ the plan.
42
+
43
+ Args:
44
+ value (dict): The value passed here should be the parameters for the POST \
45
+ request, taking the form:
46
+ {
47
+ "name": "plan_name",
48
+ "params": {
49
+ "experiment_hutch": f"{hutch_name}",
50
+ "access_device": "access_control",
51
+ "other_params": "...",
52
+ ...
53
+ }
54
+ }
55
+ """
56
+ # Value here vould be request params dictionary.
57
+ request_params = json.dumps(value)
58
+
59
+ async with ClientSession(base_url=self.url, raise_for_status=True) as session:
60
+ # First submit the plan to the worker
61
+ async with session.post(
62
+ "/tasks", data=request_params, headers=HEADERS
63
+ ) as response:
64
+ LOGGER.info(
65
+ f"Task submitted to the worker, response status: {response.status}"
66
+ )
67
+
68
+ try:
69
+ data = await response.json()
70
+ task_id = data["task_id"]
71
+ except Exception as e:
72
+ LOGGER.error(
73
+ f"Failed to get task_id from {self.url}/tasks POST. ({e})"
74
+ )
75
+ raise
76
+ # Then set the task as active and run asap
77
+ async with session.put(
78
+ "/worker/task", data=json.dumps({"task_id": task_id}), headers=HEADERS
79
+ ) as response:
80
+ if not response.ok:
81
+ LOGGER.error(
82
+ f"""Session PUT responded with {response.status}: {response.reason}.
83
+ Unable to run plan {value["name"]}.""" # type: ignore
84
+ )
85
+ return
86
+ LOGGER.info(f"Running plan: {value['name']}, task_id: {task_id}") # type: ignore
87
+
88
+ # Poll server at 2Hz until plan complete or errored
89
+ interval = 0.5
90
+ plan_complete = False
91
+
92
+ while not plan_complete:
93
+ async with session.get(f"/tasks/{task_id}") as res:
94
+ plan_result = await res.json()
95
+ plan_complete = plan_result["is_complete"]
96
+ errors = plan_result["errors"]
97
+ if len(errors) > 0:
98
+ message = "\n".join(errors)
99
+ LOGGER.error(f"Plan {value['name']} failed: {message}") # type:ignore
100
+ raise RuntimeError(f"Plan failed with error: {message}")
101
+ await asyncio.sleep(interval)
102
+ LOGGER.info(f"Plan {value['name']} done.") # type: ignore
@@ -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,41 @@
1
- from enum import Enum
2
-
3
- from bluesky.protocols import Movable
4
- from ophyd_async.core import AsyncStatus, StandardReadable
1
+ from ophyd_async.core import AsyncStatus, StandardReadableFormat
5
2
  from ophyd_async.epics.core import epics_signal_r
6
3
 
7
- from dodal.devices.hutch_shutter import HutchShutter, ShutterDemand
8
- from dodal.log import LOGGER
9
-
10
-
11
- class HutchInvalidError(Exception):
12
- pass
4
+ from dodal.devices.hutch_shutter import ShutterDemand, ShutterState
5
+ from dodal.devices.i19.blueapi_device import HutchState, OpticsBlueAPIDevice
6
+ from dodal.devices.i19.hutch_access import ACCESS_DEVICE_NAME
13
7
 
14
8
 
15
- class HutchState(str, Enum):
16
- EH1 = "EH1"
17
- EH2 = "EH2"
18
- INVALID = "INVALID"
19
-
20
-
21
- class HutchConditionalShutter(StandardReadable, Movable[ShutterDemand]):
9
+ class AccessControlledShutter(OpticsBlueAPIDevice):
22
10
  """ I19-specific device to operate the hutch shutter.
23
11
 
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.
12
+ This device will send a REST call to the blueapi instance controlling the optics \
13
+ hutch running on the I19 cluster, which will evaluate the current hutch in use vs \
14
+ the hutch sending the request and decide if the plan will be run or not.
27
15
  As the two hutches are located in series, checking the hutch in use is necessary to \
28
16
  avoid accidentally operating the shutter from one hutch while the other has beamtime.
29
17
 
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".
18
+ The name of the hutch that wants to operate the shutter should be passed to the \
19
+ device upon instantiation.
20
+
21
+ For details see the architecture described in \
22
+ https://github.com/DiamondLightSource/i19-bluesky/issues/30.
34
23
  """
35
24
 
36
25
  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")
26
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
27
+ self.shutter_status = epics_signal_r(ShutterState, f"{prefix}STA")
40
28
  self.hutch_request = hutch
41
29
  super().__init__(name)
42
30
 
43
31
  @AsyncStatus.wrap
44
32
  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)
33
+ REQUEST_PARAMS = {
34
+ "name": "operate_shutter_plan",
35
+ "params": {
36
+ "experiment_hutch": self.hutch_request.value,
37
+ "access_device": ACCESS_DEVICE_NAME,
38
+ "shutter_demand": value.value,
39
+ },
40
+ }
41
+ await super().set(REQUEST_PARAMS)
@@ -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/motors.py CHANGED
@@ -38,3 +38,24 @@ class XYZPositioner(StandardReadable):
38
38
  self.y = Motor(prefix + infix[1])
39
39
  self.z = Motor(prefix + infix[2])
40
40
  super().__init__(name=name)
41
+
42
+
43
+ class SixAxisGonio(XYZPositioner):
44
+ def __init__(
45
+ self,
46
+ prefix: str,
47
+ name: str = "",
48
+ infix: tuple[str, str, str, str, str, str] = (
49
+ "X",
50
+ "Y",
51
+ "Z",
52
+ "KAPPA",
53
+ "PHI",
54
+ "OMEGA",
55
+ ),
56
+ ):
57
+ with self.add_children_as_readables():
58
+ self.kappa = Motor(prefix + infix[3])
59
+ self.phi = Motor(prefix + infix[4])
60
+ self.omega = Motor(prefix + infix[5])
61
+ super().__init__(name=name, prefix=prefix, infix=infix[0:3])
@@ -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
 
@@ -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
+ )
@@ -1,8 +1,9 @@
1
- from ophyd_async.core import StandardReadable
1
+ from bluesky.protocols import Movable
2
+ from ophyd_async.core import AsyncStatus, StandardReadable
2
3
  from ophyd_async.epics.motor import Motor
3
4
 
4
5
 
5
- class TurboSlit(StandardReadable):
6
+ class TurboSlit(StandardReadable, Movable[float]):
6
7
  """
7
8
  This collection of motors coordinates time resolved XAS experiments.
8
9
  It selects a beam out of the polychromatic fan.
@@ -23,3 +24,8 @@ class TurboSlit(StandardReadable):
23
24
  self.arc = Motor(prefix=prefix + "ARC")
24
25
  self.xfine = Motor(prefix=prefix + "XFINE")
25
26
  super().__init__(name=name)
27
+
28
+ @AsyncStatus.wrap
29
+ async def set(self, value: float):
30
+ """This will move the default XFINE"""
31
+ await self.xfine.set(value)