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.
Files changed (85) hide show
  1. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/METADATA +27 -42
  2. dls_dodal-1.30.0.dist-info/RECORD +132 -0
  3. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/WHEEL +1 -1
  4. dls_dodal-1.30.0.dist-info/entry_points.txt +3 -0
  5. dodal/__init__.py +1 -4
  6. dodal/_version.py +2 -2
  7. dodal/beamlines/__init__.py +3 -1
  8. dodal/beamlines/i03.py +28 -23
  9. dodal/beamlines/i04.py +34 -12
  10. dodal/beamlines/i13_1.py +66 -0
  11. dodal/beamlines/i22.py +5 -5
  12. dodal/beamlines/i24.py +1 -1
  13. dodal/beamlines/p38.py +7 -7
  14. dodal/beamlines/p45.py +7 -5
  15. dodal/beamlines/p99.py +61 -0
  16. dodal/cli.py +6 -3
  17. dodal/common/beamlines/beamline_parameters.py +2 -2
  18. dodal/common/beamlines/beamline_utils.py +6 -5
  19. dodal/common/maths.py +1 -3
  20. dodal/common/types.py +2 -3
  21. dodal/common/udc_directory_provider.py +14 -3
  22. dodal/common/visit.py +2 -3
  23. dodal/devices/CTAB.py +22 -17
  24. dodal/devices/aperturescatterguard.py +114 -136
  25. dodal/devices/areadetector/adaravis.py +8 -6
  26. dodal/devices/areadetector/adsim.py +2 -3
  27. dodal/devices/areadetector/adutils.py +20 -12
  28. dodal/devices/cryostream.py +19 -7
  29. dodal/devices/detector/__init__.py +13 -2
  30. dodal/devices/detector/det_dim_constants.py +2 -2
  31. dodal/devices/detector/det_dist_to_beam_converter.py +1 -1
  32. dodal/devices/detector/detector.py +5 -5
  33. dodal/devices/detector/detector_motion.py +38 -31
  34. dodal/devices/eiger.py +11 -15
  35. dodal/devices/eiger_odin.py +9 -10
  36. dodal/devices/fast_grid_scan.py +4 -3
  37. dodal/devices/fluorescence_detector_motion.py +13 -4
  38. dodal/devices/focusing_mirror.py +4 -4
  39. dodal/devices/hutch_shutter.py +4 -4
  40. dodal/devices/i22/dcm.py +4 -3
  41. dodal/devices/i22/fswitch.py +4 -4
  42. dodal/devices/i22/nxsas.py +23 -32
  43. dodal/devices/i24/pmac.py +47 -8
  44. dodal/devices/ipin.py +7 -4
  45. dodal/devices/linkam3.py +11 -5
  46. dodal/devices/logging_ophyd_device.py +1 -1
  47. dodal/devices/motors.py +31 -5
  48. dodal/devices/oav/grid_overlay.py +1 -0
  49. dodal/devices/oav/microns_for_zoom_levels.json +1 -1
  50. dodal/devices/oav/oav_detector.py +2 -1
  51. dodal/devices/oav/oav_parameters.py +18 -10
  52. dodal/devices/oav/oav_to_redis_forwarder.py +100 -0
  53. dodal/devices/oav/pin_image_recognition/__init__.py +6 -6
  54. dodal/devices/oav/pin_image_recognition/utils.py +5 -6
  55. dodal/devices/oav/utils.py +2 -2
  56. dodal/devices/p99/__init__.py +0 -0
  57. dodal/devices/p99/sample_stage.py +43 -0
  58. dodal/devices/robot.py +30 -18
  59. dodal/devices/scintillator.py +8 -5
  60. dodal/devices/smargon.py +3 -3
  61. dodal/devices/status.py +2 -31
  62. dodal/devices/tetramm.py +4 -4
  63. dodal/devices/thawer.py +5 -3
  64. dodal/devices/undulator_dcm.py +6 -8
  65. dodal/devices/util/adjuster_plans.py +2 -2
  66. dodal/devices/util/epics_util.py +5 -7
  67. dodal/devices/util/lookup_tables.py +2 -3
  68. dodal/devices/util/save_panda.py +87 -0
  69. dodal/devices/util/test_utils.py +17 -0
  70. dodal/devices/webcam.py +3 -3
  71. dodal/devices/xbpm_feedback.py +0 -23
  72. dodal/devices/zebra.py +10 -10
  73. dodal/devices/zebra_controlled_shutter.py +3 -3
  74. dodal/devices/zocalo/zocalo_interaction.py +10 -2
  75. dodal/devices/zocalo/zocalo_results.py +31 -18
  76. dodal/log.py +14 -5
  77. dodal/plans/data_session_metadata.py +1 -0
  78. dodal/plans/motor_util_plans.py +117 -0
  79. dodal/utils.py +65 -22
  80. dls_dodal-1.29.4.dist-info/RECORD +0 -125
  81. dls_dodal-1.29.4.dist-info/entry_points.txt +0 -2
  82. dodal/devices/qbpm1.py +0 -8
  83. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/LICENSE +0 -0
  84. {dls_dodal-1.29.4.dist-info → dls_dodal-1.30.0.dist-info}/top_level.txt +0 -0
  85. /dodal/devices/i24/{I24_detector_motion.py → i24_detector_motion.py} +0 -0
@@ -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 num, func in enumerate(list(reversed(functions_to_chain))[1:-1]):
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(starting_status, functions_to_chain[0], wrapped_funcs[-1])
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.set_readable_signals([self.last_saved_path])
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((await response.read()))
27
+ await file.write(await response.read())
28
28
 
29
29
  @AsyncStatus.wrap
30
30
  async def trigger(self) -> None:
@@ -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
- def set(self, demand: ArmDemand) -> AsyncStatus:
114
- return AsyncStatus(
115
- asyncio.wait_for(self._set_armed(demand), timeout=self.TIMEOUT)
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: List[bool]) -> int:
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: List[int] = []
249
- self.invert: List[bool] = []
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(zip(self.sources, self.invert)):
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, desired_position: ZebraShutterState):
33
- await self.position_setpoint.set(desired_position)
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=desired_position,
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": getpass.getuser(),
60
- "zocalo.go.host": socket.gethostname(),
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, Generator, Sequence, Tuple, TypedDict
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, _ = soft_signal_r_and_setter(list[XrcResult], name="results")
83
- self.centres_of_mass, _ = soft_signal_r_and_setter(
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, _ = soft_signal_r_and_setter(
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, _ = soft_signal_r_and_setter(int, name="ispyb_dcid")
90
- self.ispyb_dcgid, _ = soft_signal_r_and_setter(int, name="ispyb_dcgid")
91
- self.set_readable_signals(
92
- read=[
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
- await self.results._backend.put(list(results))
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
- await self.centres_of_mass._backend.put(centres_of_mass)
107
- await self.bbox_sizes._backend.put(bbox_sizes)
108
- await self.ispyb_dcid._backend.put(ispyb_ids["dcid"])
109
- await self.ispyb_dcgid._backend.put(ispyb_ids["dcgid"])
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
- self._subscribe_to_results()
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, Tuple[np.ndarray, np.ndarray] | Tuple[None, None]]:
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 Deque, Tuple, TypedDict
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: Deque[logging.LogRecord] = deque(maxlen=capacity)
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
- ) -> Tuple[str, int]:
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)