dls-dodal 1.29.4__py3-none-any.whl → 1.31.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.31.0.dist-info}/METADATA +29 -44
- dls_dodal-1.31.0.dist-info/RECORD +134 -0
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/WHEEL +1 -1
- dls_dodal-1.31.0.dist-info/entry_points.txt +3 -0
- dodal/__init__.py +1 -4
- dodal/_version.py +2 -2
- dodal/beamline_specific_utils/i03.py +1 -4
- dodal/beamlines/__init__.py +7 -1
- dodal/beamlines/i03.py +34 -29
- dodal/beamlines/i04.py +39 -16
- dodal/beamlines/i13_1.py +66 -0
- dodal/beamlines/i22.py +22 -22
- dodal/beamlines/i24.py +1 -1
- dodal/beamlines/p38.py +21 -21
- dodal/beamlines/p45.py +18 -16
- dodal/beamlines/p99.py +61 -0
- dodal/beamlines/training_rig.py +64 -0
- dodal/cli.py +6 -3
- dodal/common/beamlines/beamline_parameters.py +7 -6
- dodal/common/beamlines/beamline_utils.py +15 -14
- dodal/common/maths.py +1 -3
- dodal/common/types.py +6 -5
- dodal/common/udc_directory_provider.py +39 -21
- dodal/common/visit.py +60 -62
- dodal/devices/CTAB.py +22 -17
- dodal/devices/aperture.py +1 -1
- dodal/devices/aperturescatterguard.py +139 -209
- dodal/devices/areadetector/adaravis.py +8 -6
- dodal/devices/areadetector/adsim.py +2 -3
- dodal/devices/areadetector/adutils.py +20 -12
- dodal/devices/areadetector/plugins/MJPG.py +2 -1
- dodal/devices/backlight.py +12 -1
- dodal/devices/cryostream.py +19 -7
- dodal/devices/dcm.py +1 -1
- 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 +33 -32
- 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 +18 -27
- dodal/devices/fluorescence_detector_motion.py +13 -4
- dodal/devices/focusing_mirror.py +6 -6
- dodal/devices/hutch_shutter.py +4 -4
- dodal/devices/i22/dcm.py +5 -4
- dodal/devices/i22/fswitch.py +10 -6
- dodal/devices/i22/nxsas.py +55 -43
- dodal/devices/i24/aperture.py +1 -1
- dodal/devices/i24/beamstop.py +1 -1
- dodal/devices/i24/dcm.py +1 -1
- dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +1 -1
- dodal/devices/i24/pmac.py +67 -12
- dodal/devices/ipin.py +7 -4
- dodal/devices/linkam3.py +12 -6
- dodal/devices/logging_ophyd_device.py +1 -1
- dodal/devices/motors.py +32 -6
- 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 +129 -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 +31 -20
- dodal/devices/scatterguard.py +1 -1
- dodal/devices/scintillator.py +8 -5
- dodal/devices/slits.py +1 -1
- dodal/devices/smargon.py +4 -4
- dodal/devices/status.py +2 -31
- dodal/devices/tetramm.py +23 -19
- dodal/devices/thawer.py +5 -3
- dodal/devices/training_rig/__init__.py +0 -0
- dodal/devices/training_rig/sample_stage.py +10 -0
- dodal/devices/turbo_slit.py +1 -1
- dodal/devices/undulator.py +1 -1
- dodal/devices/undulator_dcm.py +6 -8
- dodal/devices/util/adjuster_plans.py +3 -3
- 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 +1 -25
- dodal/devices/xspress3/xspress3.py +1 -1
- dodal/devices/zebra.py +15 -10
- dodal/devices/zebra_controlled_shutter.py +26 -11
- dodal/devices/zocalo/zocalo_interaction.py +10 -2
- dodal/devices/zocalo/zocalo_results.py +36 -19
- dodal/log.py +46 -15
- dodal/plans/check_topup.py +65 -10
- dodal/plans/data_session_metadata.py +8 -9
- 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/beamstop.py +0 -8
- dodal/devices/qbpm1.py +0 -8
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/LICENSE +0 -0
- {dls_dodal-1.29.4.dist-info → dls_dodal-1.31.0.dist-info}/top_level.txt +0 -0
|
@@ -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.fastcs.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.motor 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,11 +1,7 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
|
|
3
|
-
import ophyd
|
|
4
3
|
from bluesky.protocols import Triggerable
|
|
5
|
-
from
|
|
6
|
-
from ophyd.status import StatusBase, SubscriptionStatus
|
|
7
|
-
from ophyd_async.core import Device, observe_value
|
|
8
|
-
from ophyd_async.core.async_status import AsyncStatus
|
|
4
|
+
from ophyd_async.core import AsyncStatus, Device, observe_value
|
|
9
5
|
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw
|
|
10
6
|
|
|
11
7
|
|
|
@@ -31,23 +27,3 @@ class XBPMFeedback(Device, Triggerable):
|
|
|
31
27
|
async for value in observe_value(self.pos_stable):
|
|
32
28
|
if value:
|
|
33
29
|
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,
|
|
@@ -38,6 +37,11 @@ TTL_SHUTTER = 2
|
|
|
38
37
|
TTL_XSPRESS3 = 3
|
|
39
38
|
TTL_PANDA = 4
|
|
40
39
|
|
|
40
|
+
# The AND gate that controls the automatic shutter
|
|
41
|
+
AUTO_SHUTTER_GATE = 2
|
|
42
|
+
# The input that triggers the automatic shutter
|
|
43
|
+
AUTO_SHUTTER_INPUT = 1
|
|
44
|
+
|
|
41
45
|
|
|
42
46
|
class ArmSource(str, Enum):
|
|
43
47
|
SOFT = "Soft"
|
|
@@ -95,7 +99,7 @@ class ArmingDevice(StandardReadable):
|
|
|
95
99
|
"""A useful device that can abstract some of the logic of arming.
|
|
96
100
|
Allows a user to just call arm.set(ArmDemand.ARM)"""
|
|
97
101
|
|
|
98
|
-
TIMEOUT = 3
|
|
102
|
+
TIMEOUT: float = 3
|
|
99
103
|
|
|
100
104
|
def __init__(self, prefix: str, name: str = "") -> None:
|
|
101
105
|
self.arm_set = epics_signal_rw(float, prefix + "PC_ARM")
|
|
@@ -110,10 +114,9 @@ class ArmingDevice(StandardReadable):
|
|
|
110
114
|
if reading == demand.value:
|
|
111
115
|
return
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
)
|
|
117
|
+
@AsyncStatus.wrap
|
|
118
|
+
async def set(self, demand: ArmDemand):
|
|
119
|
+
await asyncio.wait_for(self._set_armed(demand), timeout=self.TIMEOUT)
|
|
117
120
|
|
|
118
121
|
|
|
119
122
|
class PositionCompare(StandardReadable):
|
|
@@ -166,7 +169,7 @@ class ZebraOutputPanel(StandardReadable):
|
|
|
166
169
|
super().__init__(name)
|
|
167
170
|
|
|
168
171
|
|
|
169
|
-
def boolean_array_to_integer(values:
|
|
172
|
+
def boolean_array_to_integer(values: list[bool]) -> int:
|
|
170
173
|
"""Converts a boolean array to integer by interpretting it in binary with LSB 0 bit
|
|
171
174
|
numbering.
|
|
172
175
|
|
|
@@ -245,8 +248,8 @@ class LogicGateConfiguration:
|
|
|
245
248
|
NUMBER_OF_INPUTS = 4
|
|
246
249
|
|
|
247
250
|
def __init__(self, input_source: int, invert: bool = False) -> None:
|
|
248
|
-
self.sources:
|
|
249
|
-
self.invert:
|
|
251
|
+
self.sources: list[int] = []
|
|
252
|
+
self.invert: list[bool] = []
|
|
250
253
|
self.add_input(input_source, invert)
|
|
251
254
|
|
|
252
255
|
def add_input(
|
|
@@ -271,7 +274,9 @@ class LogicGateConfiguration:
|
|
|
271
274
|
|
|
272
275
|
def __str__(self) -> str:
|
|
273
276
|
input_strings = []
|
|
274
|
-
for input, (source, invert) in enumerate(
|
|
277
|
+
for input, (source, invert) in enumerate(
|
|
278
|
+
zip(self.sources, self.invert, strict=False)
|
|
279
|
+
):
|
|
275
280
|
input_strings.append(f"INP{input+1}={'!' if invert else ''}{source}")
|
|
276
281
|
|
|
277
282
|
return ", ".join(input_strings)
|
|
@@ -7,7 +7,7 @@ from ophyd_async.core import (
|
|
|
7
7
|
StandardReadable,
|
|
8
8
|
wait_for_value,
|
|
9
9
|
)
|
|
10
|
-
from ophyd_async.epics.signal import epics_signal_r, epics_signal_w
|
|
10
|
+
from ophyd_async.epics.signal import epics_signal_r, epics_signal_rw, epics_signal_w
|
|
11
11
|
|
|
12
12
|
|
|
13
13
|
class ZebraShutterState(str, Enum):
|
|
@@ -15,24 +15,39 @@ class ZebraShutterState(str, Enum):
|
|
|
15
15
|
OPEN = "Open"
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
class ZebraShutterControl(str, Enum):
|
|
19
|
+
MANUAL = "Manual"
|
|
20
|
+
AUTO = "Auto"
|
|
21
|
+
|
|
22
|
+
|
|
18
23
|
class ZebraShutter(StandardReadable, Movable):
|
|
24
|
+
"""The shutter on most MX beamlines is controlled by the zebra.
|
|
25
|
+
|
|
26
|
+
Internally in the zebra there are two AND gates, one for manual control and one for
|
|
27
|
+
automatic control. A soft input (aliased to control_mode) will switch between
|
|
28
|
+
which of these AND gates to use. For the manual gate the shutter is then controlled
|
|
29
|
+
by a different soft input (aliased to _manual_position_setpoint). Both these AND
|
|
30
|
+
gates then feed into an OR gate, which then feeds to the shutter."""
|
|
31
|
+
|
|
19
32
|
def __init__(self, prefix: str, name: str):
|
|
20
|
-
self.
|
|
21
|
-
|
|
22
|
-
datatype=ZebraShutterState,
|
|
33
|
+
self._manual_position_setpoint = epics_signal_w(
|
|
34
|
+
ZebraShutterState, prefix + "CTRL2"
|
|
23
35
|
)
|
|
36
|
+
self.control_mode = epics_signal_rw(ZebraShutterControl, prefix + "CTRL1")
|
|
37
|
+
|
|
24
38
|
with self.add_children_as_readables():
|
|
25
|
-
self.position_readback = epics_signal_r(
|
|
26
|
-
read_pv=prefix + "STA",
|
|
27
|
-
datatype=ZebraShutterState,
|
|
28
|
-
)
|
|
39
|
+
self.position_readback = epics_signal_r(ZebraShutterState, prefix + "STA")
|
|
29
40
|
super().__init__(name=name)
|
|
30
41
|
|
|
31
42
|
@AsyncStatus.wrap
|
|
32
|
-
async def set(self,
|
|
33
|
-
await self.
|
|
43
|
+
async def set(self, value: ZebraShutterState):
|
|
44
|
+
if await self.control_mode.get_value() == ZebraShutterControl.AUTO:
|
|
45
|
+
raise UserWarning(
|
|
46
|
+
f"Tried to set shutter to {value.value} but the shutter is in auto mode."
|
|
47
|
+
)
|
|
48
|
+
await self._manual_position_setpoint.set(value)
|
|
34
49
|
return await wait_for_value(
|
|
35
50
|
signal=self.position_readback,
|
|
36
|
-
match=
|
|
51
|
+
match=value,
|
|
37
52
|
timeout=DEFAULT_TIMEOUT,
|
|
38
53
|
)
|
|
@@ -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,8 +11,12 @@ 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
|
|
14
|
-
|
|
14
|
+
from ophyd_async.core import (
|
|
15
|
+
AsyncStatus,
|
|
16
|
+
HintedSignal,
|
|
17
|
+
StandardReadable,
|
|
18
|
+
soft_signal_r_and_setter,
|
|
19
|
+
)
|
|
15
20
|
from workflows.transport.common_transport import CommonTransport
|
|
16
21
|
|
|
17
22
|
from dodal.devices.zocalo.zocalo_interaction import _get_zocalo_connection
|
|
@@ -79,34 +84,41 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
79
84
|
self._raw_results_received: Queue = Queue()
|
|
80
85
|
self.transport: CommonTransport | None = None
|
|
81
86
|
|
|
82
|
-
self.results,
|
|
83
|
-
|
|
87
|
+
self.results, self._results_setter = soft_signal_r_and_setter(
|
|
88
|
+
list[XrcResult], name="results"
|
|
89
|
+
)
|
|
90
|
+
self.centres_of_mass, self._com_setter = soft_signal_r_and_setter(
|
|
84
91
|
NDArray[np.uint64], name="centres_of_mass"
|
|
85
92
|
)
|
|
86
|
-
self.bbox_sizes,
|
|
93
|
+
self.bbox_sizes, self._bbox_setter = soft_signal_r_and_setter(
|
|
87
94
|
NDArray[np.uint64], "bbox_sizes", self.name
|
|
88
95
|
)
|
|
89
|
-
self.ispyb_dcid,
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
self.ispyb_dcid, self._ispyb_dcid_setter = soft_signal_r_and_setter(
|
|
97
|
+
int, name="ispyb_dcid"
|
|
98
|
+
)
|
|
99
|
+
self.ispyb_dcgid, self._ispyb_dcgid_setter = soft_signal_r_and_setter(
|
|
100
|
+
int, name="ispyb_dcgid"
|
|
101
|
+
)
|
|
102
|
+
self.add_readables(
|
|
103
|
+
[
|
|
93
104
|
self.results,
|
|
94
105
|
self.centres_of_mass,
|
|
95
106
|
self.bbox_sizes,
|
|
96
107
|
self.ispyb_dcid,
|
|
97
108
|
self.ispyb_dcgid,
|
|
98
|
-
]
|
|
109
|
+
],
|
|
110
|
+
wrapper=HintedSignal,
|
|
99
111
|
)
|
|
100
112
|
super().__init__(name)
|
|
101
113
|
|
|
102
114
|
async def _put_results(self, results: Sequence[XrcResult], ispyb_ids):
|
|
103
|
-
|
|
115
|
+
self._results_setter(list(results))
|
|
104
116
|
centres_of_mass = np.array([r["centre_of_mass"] for r in results])
|
|
105
117
|
bbox_sizes = np.array([bbox_size(r) for r in results])
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
118
|
+
self._com_setter(centres_of_mass)
|
|
119
|
+
self._bbox_setter(bbox_sizes)
|
|
120
|
+
self._ispyb_dcid_setter(ispyb_ids["dcid"])
|
|
121
|
+
self._ispyb_dcgid_setter(ispyb_ids["dcgid"])
|
|
110
122
|
|
|
111
123
|
def _clear_old_results(self):
|
|
112
124
|
LOGGER.info("Clearing queue")
|
|
@@ -120,7 +132,12 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
120
132
|
before triggering processing for the experiment"""
|
|
121
133
|
|
|
122
134
|
LOGGER.info("Subscribing to results queue")
|
|
123
|
-
|
|
135
|
+
try:
|
|
136
|
+
self._subscribe_to_results()
|
|
137
|
+
except Exception as e:
|
|
138
|
+
print(f"GOT {e}")
|
|
139
|
+
raise
|
|
140
|
+
|
|
124
141
|
await asyncio.sleep(CLEAR_QUEUE_WAIT_S)
|
|
125
142
|
self._clear_old_results()
|
|
126
143
|
|
|
@@ -152,7 +169,7 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
152
169
|
)
|
|
153
170
|
|
|
154
171
|
raw_results = self._raw_results_received.get(timeout=self.timeout_s)
|
|
155
|
-
LOGGER.info(f"Zocalo: found {len(raw_results)} crystals.")
|
|
172
|
+
LOGGER.info(f"Zocalo: found {len(raw_results['results'])} crystals.")
|
|
156
173
|
# Sort from strongest to weakest in case of multiple crystals
|
|
157
174
|
await self._put_results(
|
|
158
175
|
sorted(
|
|
@@ -242,7 +259,7 @@ class ZocaloResults(StandardReadable, Triggerable):
|
|
|
242
259
|
|
|
243
260
|
def get_processing_result(
|
|
244
261
|
zocalo: ZocaloResults,
|
|
245
|
-
) -> Generator[Any, Any,
|
|
262
|
+
) -> Generator[Any, Any, tuple[np.ndarray, np.ndarray] | tuple[None, None]]:
|
|
246
263
|
"""A minimal plan which will extract the top ranked xray centre and crystal bounding
|
|
247
264
|
box size from the zocalo results. Returns (None, None) if no crystals were found."""
|
|
248
265
|
|
dodal/log.py
CHANGED
|
@@ -6,30 +6,52 @@ 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
|
+
import colorlog
|
|
11
12
|
from bluesky.log import logger as bluesky_logger
|
|
12
13
|
from graypy import GELFTCPHandler
|
|
13
14
|
from ophyd.log import logger as ophyd_logger
|
|
14
|
-
from ophyd_async.log import (
|
|
15
|
-
DEFAULT_DATE_FORMAT,
|
|
16
|
-
DEFAULT_FORMAT,
|
|
17
|
-
DEFAULT_LOG_COLORS,
|
|
18
|
-
ColoredFormatterWithDeviceName,
|
|
19
|
-
)
|
|
20
|
-
from ophyd_async.log import logger as ophyd_async_logger
|
|
21
15
|
|
|
22
16
|
LOGGER = logging.getLogger("Dodal")
|
|
17
|
+
# Temporarily duplicated https://github.com/bluesky/ophyd-async/issues/550
|
|
18
|
+
ophyd_async_logger = logging.getLogger("ophyd_async")
|
|
23
19
|
LOGGER.setLevel(logging.DEBUG)
|
|
24
20
|
|
|
25
|
-
DEFAULT_FORMATTER = ColoredFormatterWithDeviceName(
|
|
26
|
-
fmt=DEFAULT_FORMAT, datefmt=DEFAULT_DATE_FORMAT, log_colors=DEFAULT_LOG_COLORS
|
|
27
|
-
)
|
|
28
21
|
ERROR_LOG_BUFFER_LINES = 20000
|
|
29
22
|
INFO_LOG_DAYS = 30
|
|
30
23
|
DEBUG_LOG_FILES_TO_KEEP = 7
|
|
31
24
|
DEFAULT_GRAYLOG_PORT = 12231
|
|
32
25
|
|
|
26
|
+
# Temporarily duplicated https://github.com/bluesky/ophyd-async/issues/550
|
|
27
|
+
DEFAULT_FORMAT = (
|
|
28
|
+
"%(log_color)s[%(levelname)1.1s %(asctime)s.%(msecs)03d "
|
|
29
|
+
"%(module)s:%(lineno)d] %(message)s"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
DEFAULT_DATE_FORMAT = "%y%m%d %H:%M:%S"
|
|
33
|
+
|
|
34
|
+
DEFAULT_LOG_COLORS = {
|
|
35
|
+
"DEBUG": "cyan",
|
|
36
|
+
"INFO": "green",
|
|
37
|
+
"WARNING": "yellow",
|
|
38
|
+
"ERROR": "red",
|
|
39
|
+
"CRITICAL": "red,bg_white",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ColoredFormatterWithDeviceName(colorlog.ColoredFormatter):
|
|
44
|
+
def format(self, record):
|
|
45
|
+
message = super().format(record)
|
|
46
|
+
if device_name := getattr(record, "ophyd_async_device_name", None):
|
|
47
|
+
message = f"[{device_name}]{message}"
|
|
48
|
+
return message
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
DEFAULT_FORMATTER = ColoredFormatterWithDeviceName(
|
|
52
|
+
fmt=DEFAULT_FORMAT, datefmt=DEFAULT_DATE_FORMAT, log_colors=DEFAULT_LOG_COLORS
|
|
53
|
+
)
|
|
54
|
+
|
|
33
55
|
|
|
34
56
|
class CircularMemoryHandler(logging.Handler):
|
|
35
57
|
"""Loosely based on the MemoryHandler, which keeps a buffer and writes it when full
|
|
@@ -37,11 +59,14 @@ class CircularMemoryHandler(logging.Handler):
|
|
|
37
59
|
that always contains the last {capacity} number of messages, this is only flushed
|
|
38
60
|
when a log of specific {flushLevel} comes in. On flush this buffer is then passed to
|
|
39
61
|
the {target} handler.
|
|
62
|
+
|
|
63
|
+
The CircularMemoryHandler becomes the owner of the target handler which will be closed
|
|
64
|
+
on close of this handler.
|
|
40
65
|
"""
|
|
41
66
|
|
|
42
67
|
def __init__(self, capacity, flushLevel=logging.ERROR, target=None):
|
|
43
68
|
logging.Handler.__init__(self)
|
|
44
|
-
self.buffer:
|
|
69
|
+
self.buffer: deque[logging.LogRecord] = deque(maxlen=capacity)
|
|
45
70
|
self.flushLevel = flushLevel
|
|
46
71
|
self.target = target
|
|
47
72
|
|
|
@@ -66,6 +91,12 @@ class CircularMemoryHandler(logging.Handler):
|
|
|
66
91
|
self.acquire()
|
|
67
92
|
try:
|
|
68
93
|
self.buffer.clear()
|
|
94
|
+
if self.target:
|
|
95
|
+
self.target.acquire()
|
|
96
|
+
try:
|
|
97
|
+
self.target.close()
|
|
98
|
+
finally:
|
|
99
|
+
self.target.release()
|
|
69
100
|
self.target = None
|
|
70
101
|
logging.Handler.close(self)
|
|
71
102
|
finally:
|
|
@@ -121,7 +152,7 @@ def set_up_graylog_handler(logger: Logger, host: str, port: int):
|
|
|
121
152
|
def set_up_INFO_file_handler(logger, path: Path, filename: str):
|
|
122
153
|
"""Set up a file handler for the logger, at INFO level, which will keep 30 days
|
|
123
154
|
of logs, rotating once per day. Creates the directory if necessary."""
|
|
124
|
-
print(f"Logging to {path/filename}")
|
|
155
|
+
print(f"Logging to INFO file handler {path/filename}")
|
|
125
156
|
path.mkdir(parents=True, exist_ok=True)
|
|
126
157
|
file_handler = TimedRotatingFileHandler(
|
|
127
158
|
filename=path / filename, when="MIDNIGHT", backupCount=INFO_LOG_DAYS
|
|
@@ -137,8 +168,8 @@ def set_up_DEBUG_memory_handler(
|
|
|
137
168
|
"""Set up a Memory handler which holds 200k lines, and writes them to an hourly
|
|
138
169
|
log file when it sees a message of severity ERROR. Creates the directory if
|
|
139
170
|
necessary"""
|
|
140
|
-
print(f"Logging to {path/filename}")
|
|
141
171
|
debug_path = path / "debug"
|
|
172
|
+
print(f"Logging to DEBUG handler {debug_path/filename}")
|
|
142
173
|
debug_path.mkdir(parents=True, exist_ok=True)
|
|
143
174
|
file_handler = TimedRotatingFileHandler(
|
|
144
175
|
filename=debug_path / filename, when="H", backupCount=DEBUG_LOG_FILES_TO_KEEP
|
|
@@ -240,7 +271,7 @@ def get_logging_file_path() -> Path:
|
|
|
240
271
|
|
|
241
272
|
def get_graylog_configuration(
|
|
242
273
|
dev_mode: bool, graylog_port: int | None = None
|
|
243
|
-
) ->
|
|
274
|
+
) -> tuple[str, int]:
|
|
244
275
|
"""Get the host and port for the graylog handler.
|
|
245
276
|
|
|
246
277
|
If running in dev mode, this switches to localhost. Otherwise it publishes to the
|
dodal/plans/check_topup.py
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
1
3
|
import bluesky.plan_stubs as bps
|
|
2
4
|
|
|
5
|
+
from dodal.common.beamlines.beamline_parameters import (
|
|
6
|
+
get_beamline_parameters,
|
|
7
|
+
)
|
|
3
8
|
from dodal.devices.synchrotron import Synchrotron, SynchrotronMode
|
|
4
9
|
from dodal.log import LOGGER
|
|
5
10
|
|
|
@@ -7,6 +12,20 @@ ALLOWED_MODES = [SynchrotronMode.USER, SynchrotronMode.SPECIAL]
|
|
|
7
12
|
DECAY_MODE_COUNTDOWN = -1 # Value of the start_countdown PV when in decay mode
|
|
8
13
|
COUNTDOWN_DURING_TOPUP = 0
|
|
9
14
|
|
|
15
|
+
DEFAULT_THRESHOLD_EXPOSURE_S = 120
|
|
16
|
+
DEFAULT_TOPUP_GATE_DELAY_S = 1
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TopupConfig:
|
|
20
|
+
# For planned exposures less than this value, wait for topup to finish instead of
|
|
21
|
+
# collecting throughout topup.
|
|
22
|
+
THRESHOLD_EXPOSURE_S = "dodal_topup_threshold_exposure_s"
|
|
23
|
+
# Additional configurable safety margin to wait after the end of topup, as the start
|
|
24
|
+
# and end countdowns do not have the same precision, and in addition we want to be sure
|
|
25
|
+
# that collection does not overlap with any transients that may occur after the
|
|
26
|
+
# nominal endpoint.
|
|
27
|
+
TOPUP_GATE_DELAY_S = "dodal_topup_end_delay_s"
|
|
28
|
+
|
|
10
29
|
|
|
11
30
|
def _in_decay_mode(time_to_topup):
|
|
12
31
|
if time_to_topup == DECAY_MODE_COUNTDOWN:
|
|
@@ -23,15 +42,38 @@ def _gating_permitted(machine_mode: SynchrotronMode):
|
|
|
23
42
|
return False
|
|
24
43
|
|
|
25
44
|
|
|
26
|
-
def _delay_to_avoid_topup(
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
45
|
+
def _delay_to_avoid_topup(
|
|
46
|
+
total_run_time_s: float,
|
|
47
|
+
time_to_topup_s: float,
|
|
48
|
+
topup_configuration: dict,
|
|
49
|
+
total_exposure_time_s: float,
|
|
50
|
+
) -> bool:
|
|
51
|
+
"""Determine whether we should delay collection until after a topup. Generally
|
|
52
|
+
if a topup is due to occur during the collection we will delay collection until after the topup.
|
|
53
|
+
However for long-running collections, impact of the topup is potentially less and collection-duration may be
|
|
54
|
+
a significant fraction of the topup-interval, therefore we may wish to collect during a topup.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
total_run_time_s: Anticipated time until end of the collection in seconds
|
|
58
|
+
time_to_topup_s: Time to the start of the topup as measured from the PV
|
|
59
|
+
topup_configuration: configuration dictionary
|
|
60
|
+
total_exposure_time_s: Total exposure time of the sample in s"""
|
|
61
|
+
if total_run_time_s > time_to_topup_s:
|
|
62
|
+
limit_s = topup_configuration.get(
|
|
63
|
+
TopupConfig.THRESHOLD_EXPOSURE_S, DEFAULT_THRESHOLD_EXPOSURE_S
|
|
33
64
|
)
|
|
34
|
-
|
|
65
|
+
gate = total_exposure_time_s < limit_s
|
|
66
|
+
if gate:
|
|
67
|
+
LOGGER.info(f"""
|
|
68
|
+
Exposure time of {total_exposure_time_s}s below the threshold of {limit_s}s.
|
|
69
|
+
Collection delayed until topup done.
|
|
70
|
+
""")
|
|
71
|
+
else:
|
|
72
|
+
LOGGER.info(f"""
|
|
73
|
+
Exposure time of {total_exposure_time_s}s meets the threshold of {limit_s}s.
|
|
74
|
+
Collection proceeding through topup.
|
|
75
|
+
""")
|
|
76
|
+
return gate
|
|
35
77
|
LOGGER.info(
|
|
36
78
|
"""
|
|
37
79
|
Total run time less than time to next topup. Proceeding with collection.
|
|
@@ -71,12 +113,25 @@ def check_topup_and_wait_if_necessary(
|
|
|
71
113
|
return
|
|
72
114
|
tot_run_time = total_exposure_time + ops_time
|
|
73
115
|
end_topup = yield from bps.rd(synchrotron.top_up_end_countdown)
|
|
74
|
-
|
|
75
|
-
|
|
116
|
+
topup_configuration = _load_topup_configuration_from_properties_file()
|
|
117
|
+
should_wait = _delay_to_avoid_topup(
|
|
118
|
+
tot_run_time,
|
|
119
|
+
time_to_topup,
|
|
120
|
+
topup_configuration,
|
|
121
|
+
total_exposure_time,
|
|
122
|
+
)
|
|
123
|
+
topup_gate_delay = topup_configuration.get(
|
|
124
|
+
TopupConfig.TOPUP_GATE_DELAY_S, DEFAULT_TOPUP_GATE_DELAY_S
|
|
76
125
|
)
|
|
126
|
+
time_to_wait = end_topup + topup_gate_delay if should_wait else 0.0
|
|
77
127
|
|
|
78
128
|
yield from bps.sleep(time_to_wait)
|
|
79
129
|
|
|
80
130
|
check_start = yield from bps.rd(synchrotron.top_up_start_countdown)
|
|
81
131
|
if check_start == COUNTDOWN_DURING_TOPUP:
|
|
82
132
|
yield from wait_for_topup_complete(synchrotron)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _load_topup_configuration_from_properties_file() -> dict[str, Any]:
|
|
136
|
+
params = get_beamline_parameters()
|
|
137
|
+
return params.params
|