dls-dodal 1.29.4__py3-none-any.whl → 1.30.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.29.4.dist-info → dls_dodal-1.30.0.dist-info}/METADATA +27 -42
- dls_dodal-1.30.0.dist-info/RECORD +132 -0
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/WHEEL +1 -1
- dls_dodal-1.30.0.dist-info/entry_points.txt +3 -0
- dodal/__init__.py +1 -4
- dodal/_version.py +2 -2
- dodal/beamlines/__init__.py +3 -1
- dodal/beamlines/i03.py +28 -23
- dodal/beamlines/i04.py +34 -12
- dodal/beamlines/i13_1.py +66 -0
- dodal/beamlines/i22.py +5 -5
- dodal/beamlines/i24.py +1 -1
- dodal/beamlines/p38.py +7 -7
- dodal/beamlines/p45.py +7 -5
- dodal/beamlines/p99.py +61 -0
- dodal/cli.py +6 -3
- dodal/common/beamlines/beamline_parameters.py +2 -2
- dodal/common/beamlines/beamline_utils.py +6 -5
- dodal/common/maths.py +1 -3
- dodal/common/types.py +2 -3
- dodal/common/udc_directory_provider.py +14 -3
- dodal/common/visit.py +2 -3
- dodal/devices/CTAB.py +22 -17
- dodal/devices/aperturescatterguard.py +114 -136
- dodal/devices/areadetector/adaravis.py +8 -6
- dodal/devices/areadetector/adsim.py +2 -3
- dodal/devices/areadetector/adutils.py +20 -12
- dodal/devices/cryostream.py +19 -7
- dodal/devices/detector/__init__.py +13 -2
- dodal/devices/detector/det_dim_constants.py +2 -2
- dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
- dodal/devices/detector/detector.py +5 -5
- dodal/devices/detector/detector_motion.py +38 -31
- dodal/devices/eiger.py +11 -15
- dodal/devices/eiger_odin.py +9 -10
- dodal/devices/fast_grid_scan.py +4 -3
- dodal/devices/fluorescence_detector_motion.py +13 -4
- dodal/devices/focusing_mirror.py +4 -4
- dodal/devices/hutch_shutter.py +4 -4
- dodal/devices/i22/dcm.py +4 -3
- dodal/devices/i22/fswitch.py +4 -4
- dodal/devices/i22/nxsas.py +23 -32
- dodal/devices/i24/pmac.py +47 -8
- dodal/devices/ipin.py +7 -4
- dodal/devices/linkam3.py +11 -5
- dodal/devices/logging_ophyd_device.py +1 -1
- dodal/devices/motors.py +31 -5
- dodal/devices/oav/grid_overlay.py +1 -0
- dodal/devices/oav/microns_for_zoom_levels.json +1 -1
- dodal/devices/oav/oav_detector.py +2 -1
- dodal/devices/oav/oav_parameters.py +18 -10
- dodal/devices/oav/oav_to_redis_forwarder.py +100 -0
- dodal/devices/oav/pin_image_recognition/__init__.py +6 -6
- dodal/devices/oav/pin_image_recognition/utils.py +5 -6
- dodal/devices/oav/utils.py +2 -2
- dodal/devices/p99/__init__.py +0 -0
- dodal/devices/p99/sample_stage.py +43 -0
- dodal/devices/robot.py +30 -18
- dodal/devices/scintillator.py +8 -5
- dodal/devices/smargon.py +3 -3
- dodal/devices/status.py +2 -31
- dodal/devices/tetramm.py +4 -4
- dodal/devices/thawer.py +5 -3
- dodal/devices/undulator_dcm.py +6 -8
- dodal/devices/util/adjuster_plans.py +2 -2
- dodal/devices/util/epics_util.py +5 -7
- dodal/devices/util/lookup_tables.py +2 -3
- dodal/devices/util/save_panda.py +87 -0
- dodal/devices/util/test_utils.py +17 -0
- dodal/devices/webcam.py +3 -3
- dodal/devices/xbpm_feedback.py +0 -23
- dodal/devices/zebra.py +10 -10
- dodal/devices/zebra_controlled_shutter.py +3 -3
- dodal/devices/zocalo/zocalo_interaction.py +10 -2
- dodal/devices/zocalo/zocalo_results.py +31 -18
- dodal/log.py +14 -5
- dodal/plans/data_session_metadata.py +1 -0
- dodal/plans/motor_util_plans.py +117 -0
- dodal/utils.py +65 -22
- dls_dodal-1.29.4.dist-info/RECORD +0 -125
- dls_dodal-1.29.4.dist-info/entry_points.txt +0 -2
- dodal/devices/qbpm1.py +0 -8
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/top_level.txt +0 -0
- /dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +0 -0
dodal/devices/util/epics_util.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
from collections.abc import Callable, Sequence
|
|
1
2
|
from functools import partial
|
|
2
|
-
from typing import Callable, Sequence
|
|
3
3
|
|
|
4
4
|
from bluesky.protocols import Movable
|
|
5
5
|
from ophyd import Component, EpicsSignal
|
|
@@ -61,9 +61,9 @@ def run_functions_without_blocking(
|
|
|
61
61
|
# Wrap each function by first checking the previous status and attaching a callback
|
|
62
62
|
# to the next function in the chain
|
|
63
63
|
def wrap_func(
|
|
64
|
-
old_status: Status, current_func: Callable[[], StatusBase], next_func
|
|
64
|
+
old_status: Status | None, current_func: Callable[[], StatusBase], next_func
|
|
65
65
|
):
|
|
66
|
-
if old_status.exception() is not None:
|
|
66
|
+
if old_status is not None and old_status.exception() is not None:
|
|
67
67
|
set_global_exception_and_log(old_status)
|
|
68
68
|
return
|
|
69
69
|
|
|
@@ -96,7 +96,7 @@ def run_functions_without_blocking(
|
|
|
96
96
|
)
|
|
97
97
|
|
|
98
98
|
# Wrap each function in reverse
|
|
99
|
-
for
|
|
99
|
+
for func in list(reversed(functions_to_chain))[1:-1]:
|
|
100
100
|
wrapped_funcs.append(
|
|
101
101
|
partial(
|
|
102
102
|
wrap_func,
|
|
@@ -105,10 +105,8 @@ def run_functions_without_blocking(
|
|
|
105
105
|
)
|
|
106
106
|
)
|
|
107
107
|
|
|
108
|
-
starting_status = Status(done=True, success=True)
|
|
109
|
-
|
|
110
108
|
# Initiate the chain of functions
|
|
111
|
-
wrap_func(
|
|
109
|
+
wrap_func(None, functions_to_chain[0], wrapped_funcs[-1])
|
|
112
110
|
return full_status
|
|
113
111
|
|
|
114
112
|
|
|
@@ -3,9 +3,8 @@ All the public methods in this module return a lookup table of some kind that
|
|
|
3
3
|
converts the source value s to a target value t for different values of s.
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
-
from collections.abc import Sequence
|
|
6
|
+
from collections.abc import Callable, Sequence
|
|
7
7
|
from io import StringIO
|
|
8
|
-
from typing import Callable
|
|
9
8
|
|
|
10
9
|
import aiofiles
|
|
11
10
|
import numpy as np
|
|
@@ -36,7 +35,7 @@ async def energy_distance_table(lookup_table_path: str) -> np.ndarray:
|
|
|
36
35
|
def linear_interpolation_lut(filename: str) -> Callable[[float], float]:
|
|
37
36
|
"""Returns a callable that converts values by linear interpolation of lookup table values"""
|
|
38
37
|
LOGGER.info(f"Using lookup table {filename}")
|
|
39
|
-
s_and_t_vals = zip(*loadtxt(filename, comments=["#", "Units"]))
|
|
38
|
+
s_and_t_vals = zip(*loadtxt(filename, comments=["#", "Units"]), strict=False)
|
|
40
39
|
|
|
41
40
|
s_values: Sequence
|
|
42
41
|
t_values: Sequence
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
from argparse import ArgumentParser
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import cast
|
|
7
|
+
|
|
8
|
+
from bluesky.run_engine import RunEngine
|
|
9
|
+
from ophyd_async.core import Device, save_device
|
|
10
|
+
from ophyd_async.panda import phase_sorter
|
|
11
|
+
|
|
12
|
+
from dodal.beamlines import module_name_for_beamline
|
|
13
|
+
from dodal.utils import make_device
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main(argv: list[str]):
|
|
17
|
+
"""CLI Utility to save the panda configuration."""
|
|
18
|
+
parser = ArgumentParser(description="Save an ophyd_async panda to yaml")
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--beamline", help="beamline to save from e.g. i03. Defaults to BEAMLINE"
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"--device-name",
|
|
24
|
+
help='name of the device. The default is "panda"',
|
|
25
|
+
default="panda",
|
|
26
|
+
)
|
|
27
|
+
parser.add_argument(
|
|
28
|
+
"-f",
|
|
29
|
+
"--force",
|
|
30
|
+
action=argparse.BooleanOptionalAction,
|
|
31
|
+
help="Force overwriting an existing file",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument("output_file", help="output filename")
|
|
34
|
+
|
|
35
|
+
# this exit()s with message/help unless args parsed successfully
|
|
36
|
+
args = parser.parse_args(argv[1:])
|
|
37
|
+
|
|
38
|
+
beamline = args.beamline
|
|
39
|
+
device_name = args.device_name
|
|
40
|
+
output_file = args.output_file
|
|
41
|
+
force = args.force
|
|
42
|
+
|
|
43
|
+
if beamline:
|
|
44
|
+
os.environ["BEAMLINE"] = beamline
|
|
45
|
+
else:
|
|
46
|
+
beamline = os.environ.get("BEAMLINE", None)
|
|
47
|
+
|
|
48
|
+
if not beamline:
|
|
49
|
+
sys.stderr.write("BEAMLINE not set and --beamline not specified\n")
|
|
50
|
+
return 1
|
|
51
|
+
|
|
52
|
+
if Path(output_file).exists() and not force:
|
|
53
|
+
sys.stderr.write(
|
|
54
|
+
f"Output file {output_file} already exists and --force not specified."
|
|
55
|
+
)
|
|
56
|
+
return 1
|
|
57
|
+
|
|
58
|
+
_save_panda(beamline, device_name, output_file)
|
|
59
|
+
|
|
60
|
+
print("Done.")
|
|
61
|
+
return 0
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _save_panda(beamline, device_name, output_file):
|
|
65
|
+
RE = RunEngine()
|
|
66
|
+
print("Creating devices...")
|
|
67
|
+
module_name = module_name_for_beamline(beamline)
|
|
68
|
+
try:
|
|
69
|
+
devices = make_device(f"dodal.beamlines.{module_name}", device_name)
|
|
70
|
+
except Exception as error:
|
|
71
|
+
sys.stderr.write(f"Couldn't create device {device_name}: {error}\n")
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
|
|
74
|
+
panda = devices[device_name]
|
|
75
|
+
print(f"Saving to {output_file} from {device_name} on {beamline}...")
|
|
76
|
+
_save_panda_to_file(RE, cast(Device, panda), output_file)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _save_panda_to_file(RE: RunEngine, panda: Device, path: str):
|
|
80
|
+
def save_to_file():
|
|
81
|
+
yield from save_device(panda, path, sorter=phase_sorter)
|
|
82
|
+
|
|
83
|
+
RE(save_to_file())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__": # pragma: no cover
|
|
87
|
+
sys.exit(main(sys.argv))
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from ophyd_async.core import (
|
|
2
|
+
callback_on_mock_put,
|
|
3
|
+
set_mock_value,
|
|
4
|
+
)
|
|
5
|
+
from ophyd_async.epics.motion import Motor
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def patch_motor(motor: Motor, initial_position=0):
|
|
9
|
+
set_mock_value(motor.user_setpoint, initial_position)
|
|
10
|
+
set_mock_value(motor.user_readback, initial_position)
|
|
11
|
+
set_mock_value(motor.deadband, 0.001)
|
|
12
|
+
set_mock_value(motor.motor_done_move, 1)
|
|
13
|
+
set_mock_value(motor.velocity, 3)
|
|
14
|
+
return callback_on_mock_put(
|
|
15
|
+
motor.user_setpoint,
|
|
16
|
+
lambda pos, *args, **kwargs: set_mock_value(motor.user_readback, pos),
|
|
17
|
+
)
|
dodal/devices/webcam.py
CHANGED
|
@@ -3,7 +3,7 @@ from pathlib import Path
|
|
|
3
3
|
import aiofiles
|
|
4
4
|
from aiohttp import ClientSession
|
|
5
5
|
from bluesky.protocols import Triggerable
|
|
6
|
-
from ophyd_async.core import AsyncStatus, StandardReadable, soft_signal_rw
|
|
6
|
+
from ophyd_async.core import AsyncStatus, HintedSignal, StandardReadable, soft_signal_rw
|
|
7
7
|
|
|
8
8
|
from dodal.log import LOGGER
|
|
9
9
|
|
|
@@ -15,7 +15,7 @@ class Webcam(StandardReadable, Triggerable):
|
|
|
15
15
|
self.directory = soft_signal_rw(str, name="directory")
|
|
16
16
|
self.last_saved_path = soft_signal_rw(str, name="last_saved_path")
|
|
17
17
|
|
|
18
|
-
self.
|
|
18
|
+
self.add_readables([self.last_saved_path], wrapper=HintedSignal)
|
|
19
19
|
super().__init__(name=name)
|
|
20
20
|
|
|
21
21
|
async def _write_image(self, file_path: str):
|
|
@@ -24,7 +24,7 @@ class Webcam(StandardReadable, Triggerable):
|
|
|
24
24
|
response.raise_for_status()
|
|
25
25
|
LOGGER.info(f"Saving webcam image from {self.url} to {file_path}")
|
|
26
26
|
async with aiofiles.open(file_path, "wb") as file:
|
|
27
|
-
await file.write(
|
|
27
|
+
await file.write(await response.read())
|
|
28
28
|
|
|
29
29
|
@AsyncStatus.wrap
|
|
30
30
|
async def trigger(self) -> None:
|
dodal/devices/xbpm_feedback.py
CHANGED
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
|
|
3
|
-
import ophyd
|
|
4
3
|
from bluesky.protocols import Triggerable
|
|
5
|
-
from ophyd import Component, EpicsSignal, EpicsSignalRO
|
|
6
|
-
from ophyd.status import StatusBase, SubscriptionStatus
|
|
7
4
|
from ophyd_async.core import Device, observe_value
|
|
8
5
|
from ophyd_async.core.async_status import AsyncStatus
|
|
9
6
|
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
|
|
@@ -31,23 +28,3 @@ class XBPMFeedback(Device, Triggerable):
|
|
|
31
28
|
async for value in observe_value(self.pos_stable):
|
|
32
29
|
if value:
|
|
33
30
|
return
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class XBPMFeedbackI04(ophyd.Device):
|
|
37
|
-
"""The I04 version of this device has a slightly different trigger method"""
|
|
38
|
-
|
|
39
|
-
# Values to set to pause_feedback
|
|
40
|
-
PAUSE = 0
|
|
41
|
-
RUN = 1
|
|
42
|
-
|
|
43
|
-
pos_ok = Component(EpicsSignalRO, "-EA-FDBK-01:XBPM2POSITION_OK")
|
|
44
|
-
pause_feedback = Component(EpicsSignal, "-EA-FDBK-01:FB_PAUSE")
|
|
45
|
-
x = Component(EpicsSignalRO, "-EA-XBPM-02:PosX:MeanValue_RBV")
|
|
46
|
-
y = Component(EpicsSignalRO, "-EA-XBPM-02:PosY:MeanValue_RBV")
|
|
47
|
-
|
|
48
|
-
def trigger(self) -> StatusBase:
|
|
49
|
-
return SubscriptionStatus(
|
|
50
|
-
self.pos_ok,
|
|
51
|
-
lambda *, old_value, value, **kwargs: value == 1,
|
|
52
|
-
timeout=60,
|
|
53
|
-
)
|
dodal/devices/zebra.py
CHANGED
|
@@ -3,7 +3,6 @@ from __future__ import annotations
|
|
|
3
3
|
import asyncio
|
|
4
4
|
from enum import Enum
|
|
5
5
|
from functools import partialmethod
|
|
6
|
-
from typing import List
|
|
7
6
|
|
|
8
7
|
from ophyd_async.core import (
|
|
9
8
|
AsyncStatus,
|
|
@@ -95,7 +94,7 @@ class ArmingDevice(StandardReadable):
|
|
|
95
94
|
"""A useful device that can abstract some of the logic of arming.
|
|
96
95
|
Allows a user to just call arm.set(ArmDemand.ARM)"""
|
|
97
96
|
|
|
98
|
-
TIMEOUT = 3
|
|
97
|
+
TIMEOUT: float = 3
|
|
99
98
|
|
|
100
99
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
101
100
|
self.arm_set = epics_signal_rw(float, prefix + "PC_ARM")
|
|
@@ -110,10 +109,9 @@ class ArmingDevice(StandardReadable):
|
|
|
110
109
|
if reading == demand.value:
|
|
111
110
|
return
|
|
112
111
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
112
|
+
@AsyncStatus.wrap
|
|
113
|
+
async def set(self, demand: ArmDemand):
|
|
114
|
+
await asyncio.wait_for(self._set_armed(demand), timeout=self.TIMEOUT)
|
|
117
115
|
|
|
118
116
|
|
|
119
117
|
class PositionCompare(StandardReadable):
|
|
@@ -166,7 +164,7 @@ class ZebraOutputPanel(StandardReadable):
|
|
|
166
164
|
super().__init__(name)
|
|
167
165
|
|
|
168
166
|
|
|
169
|
-
def boolean_array_to_integer(values:
|
|
167
|
+
def boolean_array_to_integer(values: list[bool]) -> int:
|
|
170
168
|
"""Converts a boolean array to integer by interpretting it in binary with LSB 0 bit
|
|
171
169
|
numbering.
|
|
172
170
|
|
|
@@ -245,8 +243,8 @@ class LogicGateConfiguration:
|
|
|
245
243
|
NUMBER_OF_INPUTS = 4
|
|
246
244
|
|
|
247
245
|
def __init__(self, input_source: int, invert: bool = False) -> None:
|
|
248
|
-
self.sources:
|
|
249
|
-
self.invert:
|
|
246
|
+
self.sources: list[int] = []
|
|
247
|
+
self.invert: list[bool] = []
|
|
250
248
|
self.add_input(input_source, invert)
|
|
251
249
|
|
|
252
250
|
def add_input(
|
|
@@ -271,7 +269,9 @@ class LogicGateConfiguration:
|
|
|
271
269
|
|
|
272
270
|
def __str__(self) -> str:
|
|
273
271
|
input_strings = []
|
|
274
|
-
for input, (source, invert) in enumerate(
|
|
272
|
+
for input, (source, invert) in enumerate(
|
|
273
|
+
zip(self.sources, self.invert, strict=False)
|
|
274
|
+
):
|
|
275
275
|
input_strings.append(f"INP{input+1}={'!' if invert else ''}{source}")
|
|
276
276
|
|
|
277
277
|
return ", ".join(input_strings)
|
|
@@ -29,10 +29,10 @@ class ZebraShutter(StandardReadable, Movable):
|
|
|
29
29
|
super().__init__(name=name)
|
|
30
30
|
|
|
31
31
|
@AsyncStatus.wrap
|
|
32
|
-
async def set(self,
|
|
33
|
-
await self.position_setpoint.set(
|
|
32
|
+
async def set(self, value: ZebraShutterState):
|
|
33
|
+
await self.position_setpoint.set(value)
|
|
34
34
|
return await wait_for_value(
|
|
35
35
|
signal=self.position_readback,
|
|
36
|
-
match=
|
|
36
|
+
match=value,
|
|
37
37
|
timeout=DEFAULT_TIMEOUT,
|
|
38
38
|
)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import getpass
|
|
3
|
+
import os
|
|
3
4
|
import socket
|
|
4
5
|
from dataclasses import dataclass
|
|
5
6
|
|
|
@@ -37,6 +38,12 @@ class ZocaloStartInfo:
|
|
|
37
38
|
message_index: int
|
|
38
39
|
|
|
39
40
|
|
|
41
|
+
def _get_zocalo_headers() -> tuple[str, str]:
|
|
42
|
+
user = os.environ.get("ZOCALO_GO_USER", getpass.getuser())
|
|
43
|
+
hostname = os.environ.get("ZOCALO_GO_HOSTNAME", socket.gethostname())
|
|
44
|
+
return user, hostname
|
|
45
|
+
|
|
46
|
+
|
|
40
47
|
class ZocaloTrigger:
|
|
41
48
|
"""This class just sends 'run_start' and 'run_end' messages to zocalo, it is
|
|
42
49
|
intended to be used in bluesky callback classes. To get results from zocalo back
|
|
@@ -55,9 +62,10 @@ class ZocaloTrigger:
|
|
|
55
62
|
"recipes": ["mimas"],
|
|
56
63
|
"parameters": parameters,
|
|
57
64
|
}
|
|
65
|
+
user, hostname = _get_zocalo_headers()
|
|
58
66
|
header = {
|
|
59
|
-
"zocalo.go.user":
|
|
60
|
-
"zocalo.go.host":
|
|
67
|
+
"zocalo.go.user": user,
|
|
68
|
+
"zocalo.go.host": hostname,
|
|
61
69
|
}
|
|
62
70
|
transport.send("processing_recipe", message, headers=header)
|
|
63
71
|
finally:
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
from collections import OrderedDict
|
|
3
|
+
from collections.abc import Generator, Sequence
|
|
3
4
|
from enum import Enum
|
|
4
5
|
from queue import Empty, Queue
|
|
5
|
-
from typing import Any,
|
|
6
|
+
from typing import Any, TypedDict
|
|
6
7
|
|
|
7
8
|
import bluesky.plan_stubs as bps
|
|
8
9
|
import numpy as np
|
|
@@ -10,7 +11,7 @@ import workflows.recipe
|
|
|
10
11
|
import workflows.transport
|
|
11
12
|
from bluesky.protocols import Descriptor, Triggerable
|
|
12
13
|
from numpy.typing import NDArray
|
|
13
|
-
from ophyd_async.core import StandardReadable, soft_signal_r_and_setter
|
|
14
|
+
from ophyd_async.core import HintedSignal, StandardReadable, soft_signal_r_and_setter
|
|
14
15
|
from ophyd_async.core.async_status import AsyncStatus
|
|
15
16
|
from workflows.transport.common_transport import CommonTransport
|
|
16
17
|
|
|
@@ -79,34 +80,41 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
79
80
|
self._raw_results_received: Queue = Queue()
|
|
80
81
|
self.transport: CommonTransport | None = None
|
|
81
82
|
|
|
82
|
-
self.results,
|
|
83
|
-
|
|
83
|
+
self.results, self._results_setter = soft_signal_r_and_setter(
|
|
84
|
+
list[XrcResult], name="results"
|
|
85
|
+
)
|
|
86
|
+
self.centres_of_mass, self._com_setter = soft_signal_r_and_setter(
|
|
84
87
|
NDArray[np.uint64], name="centres_of_mass"
|
|
85
88
|
)
|
|
86
|
-
self.bbox_sizes,
|
|
89
|
+
self.bbox_sizes, self._bbox_setter = soft_signal_r_and_setter(
|
|
87
90
|
NDArray[np.uint64], "bbox_sizes", self.name
|
|
88
91
|
)
|
|
89
|
-
self.ispyb_dcid,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
self.ispyb_dcid, self._ispyb_dcid_setter = soft_signal_r_and_setter(
|
|
93
|
+
int, name="ispyb_dcid"
|
|
94
|
+
)
|
|
95
|
+
self.ispyb_dcgid, self._ispyb_dcgid_setter = soft_signal_r_and_setter(
|
|
96
|
+
int, name="ispyb_dcgid"
|
|
97
|
+
)
|
|
98
|
+
self.add_readables(
|
|
99
|
+
[
|
|
93
100
|
self.results,
|
|
94
101
|
self.centres_of_mass,
|
|
95
102
|
self.bbox_sizes,
|
|
96
103
|
self.ispyb_dcid,
|
|
97
104
|
self.ispyb_dcgid,
|
|
98
|
-
]
|
|
105
|
+
],
|
|
106
|
+
wrapper=HintedSignal,
|
|
99
107
|
)
|
|
100
108
|
super().__init__(name)
|
|
101
109
|
|
|
102
110
|
async def _put_results(self, results: Sequence[XrcResult], ispyb_ids):
|
|
103
|
-
|
|
111
|
+
self._results_setter(list(results))
|
|
104
112
|
centres_of_mass = np.array([r["centre_of_mass"] for r in results])
|
|
105
113
|
bbox_sizes = np.array([bbox_size(r) for r in results])
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
114
|
+
self._com_setter(centres_of_mass)
|
|
115
|
+
self._bbox_setter(bbox_sizes)
|
|
116
|
+
self._ispyb_dcid_setter(ispyb_ids["dcid"])
|
|
117
|
+
self._ispyb_dcgid_setter(ispyb_ids["dcgid"])
|
|
110
118
|
|
|
111
119
|
def _clear_old_results(self):
|
|
112
120
|
LOGGER.info("Clearing queue")
|
|
@@ -120,7 +128,12 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
120
128
|
before triggering processing for the experiment"""
|
|
121
129
|
|
|
122
130
|
LOGGER.info("Subscribing to results queue")
|
|
123
|
-
|
|
131
|
+
try:
|
|
132
|
+
self._subscribe_to_results()
|
|
133
|
+
except Exception as e:
|
|
134
|
+
print(f"GOT {e}")
|
|
135
|
+
raise
|
|
136
|
+
|
|
124
137
|
await asyncio.sleep(CLEAR_QUEUE_WAIT_S)
|
|
125
138
|
self._clear_old_results()
|
|
126
139
|
|
|
@@ -152,7 +165,7 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
152
165
|
)
|
|
153
166
|
|
|
154
167
|
raw_results = self._raw_results_received.get(timeout=self.timeout_s)
|
|
155
|
-
LOGGER.info(f"Zocalo: found {len(raw_results)} crystals.")
|
|
168
|
+
LOGGER.info(f"Zocalo: found {len(raw_results['results'])} crystals.")
|
|
156
169
|
# Sort from strongest to weakest in case of multiple crystals
|
|
157
170
|
await self._put_results(
|
|
158
171
|
sorted(
|
|
@@ -242,7 +255,7 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
242
255
|
|
|
243
256
|
def get_processing_result(
|
|
244
257
|
zocalo: ZocaloResults,
|
|
245
|
-
) -> Generator[Any, Any,
|
|
258
|
+
) -> Generator[Any, Any, tuple[np.ndarray, np.ndarray] | tuple[None, None]]:
|
|
246
259
|
"""A minimal plan which will extract the top ranked xray centre and crystal bounding
|
|
247
260
|
box size from the zocalo results. Returns (None, None) if no crystals were found."""
|
|
248
261
|
|
dodal/log.py
CHANGED
|
@@ -6,7 +6,7 @@ from logging import Logger, StreamHandler
|
|
|
6
6
|
from logging.handlers import TimedRotatingFileHandler
|
|
7
7
|
from os import environ
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from typing import
|
|
9
|
+
from typing import TypedDict
|
|
10
10
|
|
|
11
11
|
from bluesky.log import logger as bluesky_logger
|
|
12
12
|
from graypy import GELFTCPHandler
|
|
@@ -37,11 +37,14 @@ class CircularMemoryHandler(logging.Handler):
|
|
|
37
37
|
that always contains the last {capacity} number of messages, this is only flushed
|
|
38
38
|
when a log of specific {flushLevel} comes in. On flush this buffer is then passed to
|
|
39
39
|
the {target} handler.
|
|
40
|
+
|
|
41
|
+
The CircularMemoryHandler becomes the owner of the target handler which will be closed
|
|
42
|
+
on close of this handler.
|
|
40
43
|
"""
|
|
41
44
|
|
|
42
45
|
def __init__(self, capacity, flushLevel=logging.ERROR, target=None):
|
|
43
46
|
logging.Handler.__init__(self)
|
|
44
|
-
self.buffer:
|
|
47
|
+
self.buffer: deque[logging.LogRecord] = deque(maxlen=capacity)
|
|
45
48
|
self.flushLevel = flushLevel
|
|
46
49
|
self.target = target
|
|
47
50
|
|
|
@@ -66,6 +69,12 @@ class CircularMemoryHandler(logging.Handler):
|
|
|
66
69
|
self.acquire()
|
|
67
70
|
try:
|
|
68
71
|
self.buffer.clear()
|
|
72
|
+
if self.target:
|
|
73
|
+
self.target.acquire()
|
|
74
|
+
try:
|
|
75
|
+
self.target.close()
|
|
76
|
+
finally:
|
|
77
|
+
self.target.release()
|
|
69
78
|
self.target = None
|
|
70
79
|
logging.Handler.close(self)
|
|
71
80
|
finally:
|
|
@@ -121,7 +130,7 @@ def set_up_graylog_handler(logger: Logger, host: str, port: int):
|
|
|
121
130
|
def set_up_INFO_file_handler(logger, path: Path, filename: str):
|
|
122
131
|
"""Set up a file handler for the logger, at INFO level, which will keep 30 days
|
|
123
132
|
of logs, rotating once per day. Creates the directory if necessary."""
|
|
124
|
-
print(f"Logging to {path/filename}")
|
|
133
|
+
print(f"Logging to INFO file handler {path/filename}")
|
|
125
134
|
path.mkdir(parents=True, exist_ok=True)
|
|
126
135
|
file_handler = TimedRotatingFileHandler(
|
|
127
136
|
filename=path / filename, when="MIDNIGHT", backupCount=INFO_LOG_DAYS
|
|
@@ -137,8 +146,8 @@ def set_up_DEBUG_memory_handler(
|
|
|
137
146
|
"""Set up a Memory handler which holds 200k lines, and writes them to an hourly
|
|
138
147
|
log file when it sees a message of severity ERROR. Creates the directory if
|
|
139
148
|
necessary"""
|
|
140
|
-
print(f"Logging to {path/filename}")
|
|
141
149
|
debug_path = path / "debug"
|
|
150
|
+
print(f"Logging to DEBUG handler {debug_path/filename}")
|
|
142
151
|
debug_path.mkdir(parents=True, exist_ok=True)
|
|
143
152
|
file_handler = TimedRotatingFileHandler(
|
|
144
153
|
filename=debug_path / filename, when="H", backupCount=DEBUG_LOG_FILES_TO_KEEP
|
|
@@ -240,7 +249,7 @@ def get_logging_file_path() -> Path:
|
|
|
240
249
|
|
|
241
250
|
def get_graylog_configuration(
|
|
242
251
|
dev_mode: bool, graylog_port: int | None = None
|
|
243
|
-
) ->
|
|
252
|
+
) -> tuple[str, int]:
|
|
244
253
|
"""Get the host and port for the graylog handler.
|
|
245
254
|
|
|
246
255
|
If running in dev mode, this switches to localhost. Otherwise it publishes to the
|
|
@@ -37,6 +37,7 @@ def attach_data_session_metadata_wrapper(
|
|
|
37
37
|
directory_info: DirectoryInfo = provider()
|
|
38
38
|
# https://github.com/DiamondLightSource/dodal/issues/452
|
|
39
39
|
# As part of 452, write each dataCollection into their own folder, then can use resource_dir directly
|
|
40
|
+
assert directory_info.prefix is not None
|
|
40
41
|
data_session = directory_info.prefix.removesuffix("-")
|
|
41
42
|
yield from bpp.inject_md_wrapper(plan, md={DATA_SESSION: data_session})
|
|
42
43
|
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import uuid
|
|
2
|
+
from collections.abc import Generator
|
|
3
|
+
from typing import Any, TypeVar
|
|
4
|
+
|
|
5
|
+
from bluesky import plan_stubs as bps
|
|
6
|
+
from bluesky.preprocessors import finalize_wrapper, pchain
|
|
7
|
+
from bluesky.utils import Msg, make_decorator
|
|
8
|
+
from ophyd_async.core import Device
|
|
9
|
+
from ophyd_async.epics.motion import Motor
|
|
10
|
+
|
|
11
|
+
from dodal.common import MsgGenerator
|
|
12
|
+
|
|
13
|
+
AnyDevice = TypeVar("AnyDevice", bound=Device)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class MoveTooLarge(Exception):
|
|
17
|
+
def __init__(
|
|
18
|
+
self, axis: Device, maximum_move: float, position: float, *args: object
|
|
19
|
+
) -> None:
|
|
20
|
+
self.axis = axis
|
|
21
|
+
self.maximum_move = maximum_move
|
|
22
|
+
self.position = position
|
|
23
|
+
super().__init__(*args)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _check_and_cache_values(
|
|
27
|
+
devices_and_positions: dict[AnyDevice, float],
|
|
28
|
+
smallest_move: float,
|
|
29
|
+
maximum_move: float,
|
|
30
|
+
) -> Generator[Msg, Any, dict[AnyDevice, float]]:
|
|
31
|
+
"""Caches the positions of all Motors on specified device if they are within
|
|
32
|
+
smallest_move of home_position. Throws MoveTooLarge if they are outside maximum_move
|
|
33
|
+
of the home_position
|
|
34
|
+
"""
|
|
35
|
+
positions = {}
|
|
36
|
+
for axis, new_position in devices_and_positions.items():
|
|
37
|
+
position = yield from bps.rd(axis)
|
|
38
|
+
if abs(position - new_position) > maximum_move:
|
|
39
|
+
raise MoveTooLarge(axis, maximum_move, position)
|
|
40
|
+
if abs(position - new_position) > smallest_move:
|
|
41
|
+
positions[axis] = position
|
|
42
|
+
return positions
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def home_and_reset_wrapper(
|
|
46
|
+
plan: MsgGenerator,
|
|
47
|
+
device: Device,
|
|
48
|
+
smallest_move: float,
|
|
49
|
+
maximum_move: float,
|
|
50
|
+
group: str | None = None,
|
|
51
|
+
wait_for_all: bool = True,
|
|
52
|
+
) -> MsgGenerator:
|
|
53
|
+
home_positions = {
|
|
54
|
+
axis: 0.0 for _, axis in device.children() if isinstance(axis, Motor)
|
|
55
|
+
}
|
|
56
|
+
return move_and_reset_wrapper(
|
|
57
|
+
plan, home_positions, smallest_move, maximum_move, group, wait_for_all
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def move_and_reset_wrapper(
|
|
62
|
+
plan: MsgGenerator,
|
|
63
|
+
device_and_positions: dict[AnyDevice, float],
|
|
64
|
+
smallest_move: float,
|
|
65
|
+
maximum_move: float,
|
|
66
|
+
group: str | None = None,
|
|
67
|
+
wait_for_all: bool = True,
|
|
68
|
+
) -> MsgGenerator:
|
|
69
|
+
"""Wrapper that does the following:
|
|
70
|
+
1. Caches the positions of all Motors on device
|
|
71
|
+
2. Throws a MoveTooLarge exception if any positions are maximum_move away from home_position
|
|
72
|
+
2. Moves any motor that is more than smallest_move away from the home_position to home_position
|
|
73
|
+
3. Runs the specified plan
|
|
74
|
+
4. Moves all motors back to their cached positions
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
plan (Callable[[], MsgGenerator]): The plan to move between homing and returning to the cache
|
|
78
|
+
device (Device): The device to move. All Motors in the device will be cached and moved
|
|
79
|
+
smallest_move (float): The smallest move that we care about doing the home and cache for.
|
|
80
|
+
Useful for not wearing out motors if you have large tolerances
|
|
81
|
+
maximum_move (float): If any Motor starts this far from the home an exception is raised
|
|
82
|
+
and no moves occur
|
|
83
|
+
home_position (float): The position to move every motor to after caching
|
|
84
|
+
group (str, optional): If set the home move will be done using the home-{group}
|
|
85
|
+
group and the reset to cache done using reset-{group}
|
|
86
|
+
wait_for_all (bool, optional): If true the home and reset to cache will be waited
|
|
87
|
+
on. If false it is left up to the caller to wait on
|
|
88
|
+
them. Defaults to True.
|
|
89
|
+
"""
|
|
90
|
+
initial_positions = yield from _check_and_cache_values(
|
|
91
|
+
device_and_positions, smallest_move, maximum_move
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
def move_to_home():
|
|
95
|
+
home_group = f"home-{group if group else str(uuid.uuid4())[:6]}"
|
|
96
|
+
for axis, position in device_and_positions.items():
|
|
97
|
+
if axis in initial_positions.keys():
|
|
98
|
+
yield from bps.abs_set(axis, position, group=home_group)
|
|
99
|
+
if wait_for_all:
|
|
100
|
+
yield from bps.wait(home_group)
|
|
101
|
+
|
|
102
|
+
def return_to_initial_position():
|
|
103
|
+
reset_group = f"reset-{group if group else str(uuid.uuid4())[:6]}"
|
|
104
|
+
for axis, position in initial_positions.items():
|
|
105
|
+
yield from bps.abs_set(axis, position, group=reset_group)
|
|
106
|
+
if wait_for_all:
|
|
107
|
+
yield from bps.wait(reset_group)
|
|
108
|
+
|
|
109
|
+
return (
|
|
110
|
+
yield from finalize_wrapper(
|
|
111
|
+
pchain(move_to_home(), plan),
|
|
112
|
+
return_to_initial_position(),
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
home_and_reset_decorator = make_decorator(home_and_reset_wrapper)
|