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.
- {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/METADATA +4 -3
- {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/RECORD +66 -49
- {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/WHEEL +1 -1
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +2 -0
- dodal/beamlines/b01_1.py +8 -0
- dodal/beamlines/b07.py +27 -0
- dodal/beamlines/b07_1.py +25 -0
- dodal/beamlines/i03.py +11 -0
- dodal/beamlines/i09.py +25 -0
- dodal/beamlines/i09_1.py +25 -0
- dodal/beamlines/i10.py +19 -35
- dodal/beamlines/i13_1.py +22 -48
- dodal/beamlines/i19_1.py +17 -5
- dodal/beamlines/i19_2.py +13 -3
- dodal/beamlines/i19_optics.py +4 -2
- dodal/beamlines/i20_1.py +2 -1
- dodal/beamlines/i23.py +10 -0
- dodal/beamlines/p60.py +21 -0
- dodal/common/data_util.py +20 -0
- dodal/common/signal_utils.py +43 -4
- dodal/common/visit.py +1 -41
- dodal/devices/aperturescatterguard.py +3 -3
- dodal/devices/baton.py +17 -0
- dodal/devices/current_amplifiers/current_amplifier.py +1 -6
- dodal/devices/current_amplifiers/current_amplifier_detector.py +2 -2
- dodal/devices/current_amplifiers/femto.py +0 -5
- dodal/devices/current_amplifiers/sr570.py +0 -5
- dodal/devices/detector/det_dist_to_beam_converter.py +16 -23
- dodal/devices/detector/detector.py +2 -1
- dodal/devices/electron_analyser/__init__.py +0 -0
- dodal/devices/electron_analyser/abstract_analyser_io.py +47 -0
- dodal/devices/electron_analyser/abstract_region.py +112 -0
- dodal/devices/electron_analyser/specs_analyser_io.py +19 -0
- dodal/devices/electron_analyser/specs_region.py +26 -0
- dodal/devices/electron_analyser/vgscienta_analyser_io.py +26 -0
- dodal/devices/electron_analyser/vgscienta_region.py +90 -0
- dodal/devices/fast_grid_scan.py +2 -2
- dodal/devices/i03/beamstop.py +2 -2
- dodal/devices/i10/diagnostics.py +239 -0
- dodal/devices/i10/slits.py +93 -6
- dodal/devices/i13_1/merlin.py +1 -2
- dodal/devices/i13_1/merlin_controller.py +12 -8
- dodal/devices/i19/beamstop.py +30 -0
- dodal/devices/i19/blueapi_device.py +102 -0
- dodal/devices/i19/hutch_access.py +2 -0
- dodal/devices/i19/shutter.py +24 -40
- dodal/devices/i22/nxsas.py +1 -3
- dodal/devices/i24/focus_mirrors.py +3 -3
- dodal/devices/i24/pilatus_metadata.py +2 -2
- dodal/devices/motors.py +21 -0
- dodal/devices/oav/oav_detector.py +7 -9
- dodal/devices/oav/snapshots/snapshot.py +21 -0
- dodal/devices/oav/snapshots/snapshot_image_processing.py +74 -0
- dodal/devices/turbo_slit.py +8 -2
- dodal/devices/undulator.py +9 -7
- dodal/devices/util/adjuster_plans.py +1 -2
- dodal/devices/util/lookup_tables.py +38 -0
- dodal/devices/util/test_utils.py +1 -0
- dodal/plan_stubs/electron_analyser/__init__.py +0 -0
- dodal/plan_stubs/electron_analyser/configure_controller.py +80 -0
- dodal/plan_stubs/motor_utils.py +10 -12
- dodal/utils.py +0 -7
- dodal/devices/i13_1/merlin_io.py +0 -17
- dodal/devices/oav/microns_for_zoom_levels.json +0 -55
- dodal/devices/oav/snapshots/snapshot_with_beam_centre.py +0 -64
- dodal/devices/util/motor_utils.py +0 -6
- {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/entry_points.txt +0 -0
- {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info/licenses}/LICENSE +0 -0
- {dls_dodal-1.43.0.dist-info → dls_dodal-1.45.0.dist-info}/top_level.txt +0 -0
dodal/devices/i10/slits.py
CHANGED
|
@@ -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
|
|
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.
|
|
10
|
-
self.
|
|
11
|
-
self.
|
|
12
|
-
self.
|
|
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)
|
dodal/devices/i13_1/merlin.py
CHANGED
|
@@ -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 =
|
|
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
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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:
|
|
19
|
-
good_states: frozenset[
|
|
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(
|
|
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
|
|
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:
|
dodal/devices/i19/shutter.py
CHANGED
|
@@ -1,57 +1,41 @@
|
|
|
1
|
-
from
|
|
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
|
|
8
|
-
from dodal.
|
|
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
|
|
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
|
|
25
|
-
|
|
26
|
-
|
|
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
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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.
|
|
38
|
-
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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)
|
dodal/devices/i22/nxsas.py
CHANGED
|
@@ -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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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 =
|
|
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
|
|
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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
+
)
|
dodal/devices/turbo_slit.py
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from
|
|
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)
|