dls-dodal 1.39.0__py3-none-any.whl → 1.41.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 (61) hide show
  1. {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/METADATA +5 -3
  2. {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/RECORD +61 -52
  3. {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +9 -4
  5. dodal/beamlines/__init__.py +2 -0
  6. dodal/beamlines/adsim.py +3 -2
  7. dodal/beamlines/b01_1.py +3 -3
  8. dodal/beamlines/i03.py +141 -292
  9. dodal/beamlines/i04.py +112 -198
  10. dodal/beamlines/i13_1.py +5 -4
  11. dodal/beamlines/i18.py +124 -0
  12. dodal/beamlines/i19_1.py +74 -0
  13. dodal/beamlines/i19_2.py +61 -0
  14. dodal/beamlines/i20_1.py +37 -22
  15. dodal/beamlines/i22.py +7 -7
  16. dodal/beamlines/i23.py +8 -11
  17. dodal/beamlines/i24.py +100 -145
  18. dodal/beamlines/p38.py +84 -220
  19. dodal/beamlines/p45.py +5 -4
  20. dodal/beamlines/training_rig.py +4 -4
  21. dodal/common/beamlines/beamline_utils.py +2 -3
  22. dodal/common/beamlines/device_helpers.py +3 -1
  23. dodal/devices/aperturescatterguard.py +150 -64
  24. dodal/devices/apple2_undulator.py +89 -114
  25. dodal/devices/attenuator/attenuator.py +1 -1
  26. dodal/devices/backlight.py +1 -1
  27. dodal/devices/bimorph_mirror.py +2 -2
  28. dodal/devices/eiger.py +3 -2
  29. dodal/devices/fast_grid_scan.py +26 -19
  30. dodal/devices/hutch_shutter.py +26 -13
  31. dodal/devices/i10/i10_apple2.py +3 -3
  32. dodal/devices/i10/rasor/rasor_scaler_cards.py +4 -4
  33. dodal/devices/i13_1/merlin.py +4 -3
  34. dodal/devices/i13_1/merlin_controller.py +2 -7
  35. dodal/devices/i18/KBMirror.py +19 -0
  36. dodal/devices/i18/diode.py +17 -0
  37. dodal/devices/i18/table.py +14 -0
  38. dodal/devices/i18/thor_labs_stage.py +12 -0
  39. dodal/devices/i19/__init__.py +0 -0
  40. dodal/devices/i19/shutter.py +57 -0
  41. dodal/devices/i22/nxsas.py +4 -4
  42. dodal/devices/i24/pmac.py +2 -2
  43. dodal/devices/motors.py +2 -2
  44. dodal/devices/oav/oav_detector.py +10 -19
  45. dodal/devices/pressure_jump_cell.py +43 -19
  46. dodal/devices/robot.py +31 -12
  47. dodal/devices/tetramm.py +8 -3
  48. dodal/devices/thawer.py +4 -4
  49. dodal/devices/turbo_slit.py +7 -6
  50. dodal/devices/undulator.py +1 -1
  51. dodal/devices/undulator_dcm.py +1 -1
  52. dodal/devices/util/epics_util.py +1 -1
  53. dodal/devices/zebra/zebra.py +4 -3
  54. dodal/devices/zebra/zebra_controlled_shutter.py +1 -1
  55. dodal/devices/zocalo/zocalo_results.py +21 -4
  56. dodal/plan_stubs/wrapped.py +10 -12
  57. dodal/plans/save_panda.py +30 -14
  58. dodal/utils.py +55 -21
  59. {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/LICENSE +0 -0
  60. {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/entry_points.txt +0 -0
  61. {dls_dodal-1.39.0.dist-info → dls_dodal-1.41.0.dist-info}/top_level.txt +0 -0
dodal/devices/robot.py CHANGED
@@ -5,6 +5,7 @@ from dataclasses import dataclass
5
5
  from bluesky.protocols import Movable
6
6
  from ophyd_async.core import (
7
7
  AsyncStatus,
8
+ Device,
8
9
  StandardReadable,
9
10
  StrictEnum,
10
11
  set_and_wait_for_value,
@@ -38,23 +39,36 @@ class PinMounted(StrictEnum):
38
39
  PIN_MOUNTED = "Pin Mounted"
39
40
 
40
41
 
41
- class BartRobot(StandardReadable, Movable):
42
+ class ErrorStatus(Device):
43
+ def __init__(self, prefix: str) -> None:
44
+ self.str = epics_signal_r(str, prefix + "_ERR_MSG")
45
+ self.code = epics_signal_r(int, prefix + "_ERR_CODE")
46
+ super().__init__()
47
+
48
+ async def raise_if_error(self, raise_from: Exception):
49
+ error_code = await self.code.get_value()
50
+ if error_code:
51
+ error_string = await self.str.get_value()
52
+ raise RobotLoadFailed(int(error_code), error_string) from raise_from
53
+
54
+
55
+ class BartRobot(StandardReadable, Movable[SampleLocation]):
42
56
  """The sample changing robot."""
43
57
 
44
58
  # How long to wait for the robot if it is busy soaking/drying
45
59
  NOT_BUSY_TIMEOUT = 5 * 60
60
+
46
61
  # How long to wait for the actual load to happen
47
62
  LOAD_TIMEOUT = 60
63
+
64
+ # Error codes that we do special things on
48
65
  NO_PIN_ERROR_CODE = 25
66
+ LIGHT_CURTAIN_TRIPPED = 40
49
67
 
50
68
  # How far the gonio position can be out before loading will fail
51
69
  LOAD_TOLERANCE_MM = 0.02
52
70
 
53
- def __init__(
54
- self,
55
- name: str,
56
- prefix: str,
57
- ) -> None:
71
+ def __init__(self, name: str, prefix: str) -> None:
58
72
  self.barcode = epics_signal_r(str, prefix + "BARCODE")
59
73
  self.gonio_pin_sensor = epics_signal_r(PinMounted, prefix + "PIN_MOUNTED")
60
74
 
@@ -69,8 +83,10 @@ class BartRobot(StandardReadable, Movable):
69
83
  self.load = epics_signal_x(prefix + "LOAD.PROC")
70
84
  self.program_running = epics_signal_r(bool, prefix + "PROGRAM_RUNNING")
71
85
  self.program_name = epics_signal_r(str, prefix + "PROGRAM_NAME")
72
- self.error_str = epics_signal_r(str, prefix + "PRG_ERR_MSG")
73
- self.error_code = epics_signal_r(int, prefix + "PRG_ERR_CODE")
86
+
87
+ self.prog_error = ErrorStatus(prefix + "PRG")
88
+ self.controller_error = ErrorStatus(prefix + "CNTL")
89
+
74
90
  self.reset = epics_signal_x(prefix + "RESET.PROC")
75
91
  super().__init__(name=name)
76
92
 
@@ -81,7 +97,7 @@ class BartRobot(StandardReadable, Movable):
81
97
  """
82
98
 
83
99
  async def raise_if_no_pin():
84
- await wait_for_value(self.error_code, self.NO_PIN_ERROR_CODE, None)
100
+ await wait_for_value(self.prog_error.code, self.NO_PIN_ERROR_CODE, None)
85
101
  raise RobotLoadFailed(self.NO_PIN_ERROR_CODE, "Pin was not detected")
86
102
 
87
103
  async def wfv():
@@ -108,6 +124,9 @@ class BartRobot(StandardReadable, Movable):
108
124
  raise
109
125
 
110
126
  async def _load_pin_and_puck(self, sample_location: SampleLocation):
127
+ if await self.controller_error.code.get_value() == self.LIGHT_CURTAIN_TRIPPED:
128
+ LOGGER.info("Light curtain tripped, trying again")
129
+ await self.reset.trigger()
111
130
  LOGGER.info(f"Loading pin {sample_location}")
112
131
  if await self.program_running.get_value():
113
132
  LOGGER.info(
@@ -137,6 +156,6 @@ class BartRobot(StandardReadable, Movable):
137
156
  )
138
157
  except (asyncio.TimeoutError, TimeoutError) as e:
139
158
  # Will only need to catch asyncio.TimeoutError after https://github.com/bluesky/ophyd-async/issues/572
140
- error_code = await self.error_code.get_value()
141
- error_string = await self.error_str.get_value()
142
- raise RobotLoadFailed(int(error_code), error_string) from e
159
+ await self.prog_error.raise_if_error(e)
160
+ await self.controller_error.raise_if_error(e)
161
+ raise RobotLoadFailed(0, "Robot timed out") from e
dodal/devices/tetramm.py CHANGED
@@ -13,7 +13,12 @@ from ophyd_async.core import (
13
13
  set_and_wait_for_value,
14
14
  soft_signal_r_and_setter,
15
15
  )
16
- from ophyd_async.epics.adcore import ADHDFWriter, NDFileHDFIO, stop_busy_record
16
+ from ophyd_async.epics.adcore import (
17
+ ADHDFWriter,
18
+ NDFileHDFIO,
19
+ NDPluginBaseIO,
20
+ stop_busy_record,
21
+ )
17
22
  from ophyd_async.epics.core import (
18
23
  epics_signal_r,
19
24
  epics_signal_rw,
@@ -221,7 +226,7 @@ class TetrammDetector(StandardDetector):
221
226
  path_provider: PathProvider,
222
227
  name: str = "",
223
228
  type: str | None = None,
224
- **scalar_sigs: str,
229
+ plugins: dict[str, NDPluginBaseIO] | None = None,
225
230
  ) -> None:
226
231
  self.drv = TetrammDriver(prefix + "DRV:")
227
232
  self.hdf = NDFileHDFIO(prefix + "HDF5:")
@@ -243,7 +248,7 @@ class TetrammDetector(StandardDetector):
243
248
  path_provider,
244
249
  lambda: self.name,
245
250
  TetrammDatasetDescriber(controller),
246
- **scalar_sigs,
251
+ plugins=plugins,
247
252
  ),
248
253
  config_signals,
249
254
  name,
dodal/devices/thawer.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from asyncio import Task, create_task, sleep
2
2
 
3
- from bluesky.protocols import Stoppable
3
+ from bluesky.protocols import Movable, Stoppable
4
4
  from ophyd_async.core import (
5
5
  AsyncStatus,
6
6
  Device,
@@ -21,18 +21,18 @@ class ThawerStates(StrictEnum):
21
21
  ON = "On"
22
22
 
23
23
 
24
- class ThawingTimer(Device, Stoppable):
24
+ class ThawingTimer(Device, Stoppable, Movable[float]):
25
25
  def __init__(self, control_signal: SignalRW[ThawerStates]) -> None:
26
26
  self._control_signal_ref = Reference(control_signal)
27
27
  self._thawing_task: Task | None = None
28
28
  super().__init__("thaw_for_time_s")
29
29
 
30
30
  @AsyncStatus.wrap
31
- async def set(self, time_to_thaw_for: float):
31
+ async def set(self, value: float):
32
32
  await self._control_signal_ref().set(ThawerStates.ON)
33
33
  if self._thawing_task and not self._thawing_task.done():
34
34
  raise ThawingException("Thawing task already in progress")
35
- self._thawing_task = create_task(sleep(time_to_thaw_for))
35
+ self._thawing_task = create_task(sleep(value))
36
36
  try:
37
37
  await self._thawing_task
38
38
  finally:
@@ -1,8 +1,8 @@
1
- from ophyd_async.core import Device
1
+ from ophyd_async.core import StandardReadable
2
2
  from ophyd_async.epics.motor import Motor
3
3
 
4
4
 
5
- class TurboSlit(Device):
5
+ class TurboSlit(StandardReadable):
6
6
  """
7
7
  This collection of motors coordinates time resolved XAS experiments.
8
8
  It selects a beam out of the polychromatic fan.
@@ -17,8 +17,9 @@ class TurboSlit(Device):
17
17
  - xfine selects the energy as part of the high frequency scan
18
18
  """
19
19
 
20
- def __init__(self, prefix: str, name: str):
21
- self.gap = Motor(prefix=prefix + "GAP")
22
- self.arc = Motor(prefix=prefix + "ARC")
23
- self.xfine = Motor(prefix=prefix + "XFINE")
20
+ def __init__(self, prefix: str, name: str = ""):
21
+ with self.add_children_as_readables():
22
+ self.gap = Motor(prefix=prefix + "GAP")
23
+ self.arc = Motor(prefix=prefix + "ARC")
24
+ self.xfine = Motor(prefix=prefix + "XFINE")
24
25
  super().__init__(name=name)
@@ -46,7 +46,7 @@ def _get_closest_gap_for_energy(
46
46
  return table[1][idx]
47
47
 
48
48
 
49
- class Undulator(StandardReadable, Movable):
49
+ class Undulator(StandardReadable, Movable[float]):
50
50
  """
51
51
  An Undulator-type insertion device, used to control photon emission at a given
52
52
  beam energy.
@@ -12,7 +12,7 @@ from .undulator import Undulator
12
12
  ENERGY_TIMEOUT_S: float = 30.0
13
13
 
14
14
 
15
- class UndulatorDCM(StandardReadable, Movable):
15
+ class UndulatorDCM(StandardReadable, Movable[float]):
16
16
  """
17
17
  Composite device to handle changing beamline energies, wraps the Undulator and the
18
18
  DCM. The DCM has a motor which controls the beam energy, when it moves, the
@@ -114,7 +114,7 @@ def call_func(func: Callable[[], StatusBase]) -> StatusBase:
114
114
  return func()
115
115
 
116
116
 
117
- class SetWhenEnabled(OphydAsyncDevice, Movable):
117
+ class SetWhenEnabled(OphydAsyncDevice, Movable[int]):
118
118
  """A device that sets the proc field of a PV when it becomes enabled."""
119
119
 
120
120
  def __init__(self, name: str = "", prefix: str = ""):
@@ -4,6 +4,7 @@ import asyncio
4
4
  from enum import Enum
5
5
  from functools import partialmethod
6
6
 
7
+ from bluesky.protocols import Movable
7
8
  from ophyd_async.core import (
8
9
  AsyncStatus,
9
10
  DeviceVector,
@@ -74,7 +75,7 @@ class SoftInState(StrictEnum):
74
75
  NO = "No"
75
76
 
76
77
 
77
- class ArmingDevice(StandardReadable):
78
+ class ArmingDevice(StandardReadable, Movable[ArmDemand]):
78
79
  """A useful device that can abstract some of the logic of arming.
79
80
  Allows a user to just call arm.set(ArmDemand.ARM)"""
80
81
 
@@ -94,8 +95,8 @@ class ArmingDevice(StandardReadable):
94
95
  return
95
96
 
96
97
  @AsyncStatus.wrap
97
- async def set(self, demand: ArmDemand):
98
- await asyncio.wait_for(self._set_armed(demand), timeout=self.TIMEOUT)
98
+ async def set(self, value: ArmDemand):
99
+ await asyncio.wait_for(self._set_armed(value), timeout=self.TIMEOUT)
99
100
 
100
101
 
101
102
  class PositionCompare(StandardReadable):
@@ -19,7 +19,7 @@ class ZebraShutterControl(StrictEnum):
19
19
  AUTO = "Auto"
20
20
 
21
21
 
22
- class ZebraShutter(StandardReadable, Movable):
22
+ class ZebraShutter(StandardReadable, Movable[ZebraShutterState]):
23
23
  """The shutter on most MX beamlines is controlled by the zebra.
24
24
 
25
25
  Internally in the zebra there are two AND gates, one for manual control and one for
@@ -129,7 +129,12 @@ class ZocaloResults(StandardReadable, Triggerable):
129
129
 
130
130
  prefix (str): EPICS PV prefix for the device
131
131
 
132
- use_cpu_and_gpu (bool): When True, ZocaloResults will wait for results from the CPU and the GPU, compare them, and provide a warning if the results differ. When False, ZocaloResults will only use results from the CPU
132
+ use_cpu_and_gpu (bool): When True, ZocaloResults will wait for results from the
133
+ CPU and the GPU, compare them, and provide a warning if the results differ. When
134
+ False, ZocaloResults will only use results from the CPU
135
+
136
+ use_gpu (bool): When True, ZocaloResults will take the first set of
137
+ results that it receives (which are likely the GPU results)
133
138
 
134
139
  """
135
140
 
@@ -142,6 +147,7 @@ class ZocaloResults(StandardReadable, Triggerable):
142
147
  timeout_s: float = DEFAULT_TIMEOUT,
143
148
  prefix: str = "",
144
149
  use_cpu_and_gpu: bool = False,
150
+ use_gpu: bool = False,
145
151
  ) -> None:
146
152
  self.zocalo_environment = zocalo_environment
147
153
  self.sort_key = SortKeys[sort_key]
@@ -151,6 +157,7 @@ class ZocaloResults(StandardReadable, Triggerable):
151
157
  self._raw_results_received: Queue = Queue()
152
158
  self.transport: CommonTransport | None = None
153
159
  self.use_cpu_and_gpu = use_cpu_and_gpu
160
+ self.use_gpu = use_gpu
154
161
 
155
162
  self.centre_of_mass, self._com_setter = soft_signal_r_and_setter(
156
163
  Array1D[np.float64], name="centre_of_mass"
@@ -213,6 +220,11 @@ class ZocaloResults(StandardReadable, Triggerable):
213
220
  clearing the queue. Plans using this device should wait on ZOCALO_STAGE_GROUP
214
221
  before triggering processing for the experiment"""
215
222
 
223
+ if self.use_cpu_and_gpu and self.use_gpu:
224
+ raise ValueError(
225
+ "Cannot compare GPU and CPU results and use GPU results at the same time."
226
+ )
227
+
216
228
  LOGGER.info("Subscribing to results queue")
217
229
  try:
218
230
  self._subscribe_to_results()
@@ -253,8 +265,13 @@ class ZocaloResults(StandardReadable, Triggerable):
253
265
  raw_results = self._raw_results_received.get(timeout=self.timeout_s)
254
266
  source_of_first_results = source_from_results(raw_results)
255
267
 
256
- # Wait for results from CPU and GPU, warn and continue if only GPU times out. Error if CPU times out
268
+ if self.use_gpu and source_of_first_results == ZocaloSource.CPU:
269
+ LOGGER.warning(
270
+ "Configured to use GPU results but CPU came first, using CPU results."
271
+ )
272
+
257
273
  if self.use_cpu_and_gpu:
274
+ # Wait for results from CPU and GPU, warn and continue if only GPU times out. Error if CPU times out
258
275
  if source_of_first_results == ZocaloSource.CPU:
259
276
  LOGGER.warning("Received zocalo results from CPU before GPU")
260
277
  raw_results_two_sources = [raw_results]
@@ -303,7 +320,7 @@ class ZocaloResults(StandardReadable, Triggerable):
303
320
  raise err
304
321
 
305
322
  LOGGER.info(
306
- f"Zocalo results from {ZocaloSource.CPU.value} processing: found {len(raw_results['results'])} crystals."
323
+ f"Zocalo results from {source_from_results(raw_results)} processing: found {len(raw_results['results'])} crystals."
307
324
  )
308
325
  # Sort from strongest to weakest in case of multiple crystals
309
326
  await self._put_results(
@@ -335,7 +352,7 @@ class ZocaloResults(StandardReadable, Triggerable):
335
352
 
336
353
  results = message.get("results", [])
337
354
 
338
- if self.use_cpu_and_gpu:
355
+ if self.use_cpu_and_gpu or self.use_gpu:
339
356
  self._raw_results_received.put(
340
357
  {"results": results, "recipe_parameters": recipe_parameters}
341
358
  )
@@ -1,6 +1,6 @@
1
1
  import itertools
2
2
  from collections.abc import Mapping
3
- from typing import Annotated, Any
3
+ from typing import Annotated, TypeVar
4
4
 
5
5
  import bluesky.plan_stubs as bps
6
6
  from bluesky.protocols import Movable
@@ -12,17 +12,17 @@ Wrappers for Bluesky built-in plan stubs with type hinting
12
12
 
13
13
  Group = Annotated[str, "String identifier used by 'wait' or stubs that await"]
14
14
 
15
+ T = TypeVar("T")
16
+
15
17
 
16
- # After bluesky 1.14, bounds for stubs that move can be narrowed
17
- # https://github.com/bluesky/bluesky/issues/1821
18
18
  def set_absolute(
19
- movable: Movable, value: Any, group: Group | None = None, wait: bool = False
19
+ movable: Movable[T], value: T, group: Group | None = None, wait: bool = False
20
20
  ) -> MsgGenerator:
21
21
  """
22
22
  Set a device, wrapper for `bp.abs_set`.
23
23
 
24
24
  Args:
25
- movable (Movable): The device to set
25
+ movable (Movable[T]): The device to set
26
26
  value (T): The new value
27
27
  group (Group | None, optional): The message group to associate with the
28
28
  setting, for sequencing. Defaults to None.
@@ -39,7 +39,7 @@ def set_absolute(
39
39
 
40
40
 
41
41
  def set_relative(
42
- movable: Movable, value: Any, group: Group | None = None, wait: bool = False
42
+ movable: Movable[T], value: T, group: Group | None = None, wait: bool = False
43
43
  ) -> MsgGenerator:
44
44
  """
45
45
  Change a device, wrapper for `bp.rel_set`.
@@ -62,7 +62,7 @@ def set_relative(
62
62
  return (yield from bps.rel_set(movable, value, group=group, wait=wait))
63
63
 
64
64
 
65
- def move(moves: Mapping[Movable, Any], group: Group | None = None) -> MsgGenerator:
65
+ def move(moves: Mapping[Movable[T], T], group: Group | None = None) -> MsgGenerator:
66
66
  """
67
67
  Move a device, wrapper for `bp.mv`.
68
68
 
@@ -79,13 +79,12 @@ def move(moves: Mapping[Movable, Any], group: Group | None = None) -> MsgGenerat
79
79
  """
80
80
 
81
81
  return (
82
- # type ignore until https://github.com/bluesky/bluesky/issues/1809
83
- yield from bps.mv(*itertools.chain.from_iterable(moves.items()), group=group) # type: ignore
82
+ yield from bps.mv(*itertools.chain.from_iterable(moves.items()), group=group)
84
83
  )
85
84
 
86
85
 
87
86
  def move_relative(
88
- moves: Mapping[Movable, Any], group: Group | None = None
87
+ moves: Mapping[Movable[T], T], group: Group | None = None
89
88
  ) -> MsgGenerator:
90
89
  """
91
90
  Move a device relative to its current position, wrapper for `bp.mvr`.
@@ -103,8 +102,7 @@ def move_relative(
103
102
  """
104
103
 
105
104
  return (
106
- # type ignore until https://github.com/bluesky/bluesky/issues/1809
107
- yield from bps.mvr(*itertools.chain.from_iterable(moves.items()), group=group) # type: ignore
105
+ yield from bps.mvr(*itertools.chain.from_iterable(moves.items()), group=group)
108
106
  )
109
107
 
110
108
 
dodal/plans/save_panda.py CHANGED
@@ -6,8 +6,10 @@ from pathlib import Path
6
6
  from typing import cast
7
7
 
8
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
9
+ from ophyd_async.core import Device, YamlSettingsProvider
10
+ from ophyd_async.plan_stubs import (
11
+ store_settings,
12
+ )
11
13
 
12
14
  from dodal.beamlines import module_name_for_beamline
13
15
  from dodal.utils import make_device
@@ -17,20 +19,24 @@ def main(argv: list[str]):
17
19
  """CLI Utility to save the panda configuration."""
18
20
  parser = ArgumentParser(description="Save an ophyd_async panda to yaml")
19
21
  parser.add_argument(
20
- "--beamline", help="beamline to save from e.g. i03. Defaults to BEAMLINE"
22
+ "--beamline", help="Beamline to save from e.g. i03. Defaults to BEAMLINE"
21
23
  )
22
24
  parser.add_argument(
23
25
  "--device-name",
24
- help='name of the device. The default is "panda"',
26
+ help='Name of the device. The default is "panda"',
25
27
  default="panda",
26
28
  )
29
+ parser.add_argument(
30
+ "--output-file",
31
+ help="Path to output file, including filename, eg '/scratch/panda_settings'. '.yaml' is appended to the file name automatically",
32
+ required=True,
33
+ )
27
34
  parser.add_argument(
28
35
  "-f",
29
36
  "--force",
30
37
  action=argparse.BooleanOptionalAction,
31
38
  help="Force overwriting an existing file",
32
39
  )
33
- parser.add_argument("output_file", help="output filename")
34
40
 
35
41
  # this exit()s with message/help unless args parsed successfully
36
42
  args = parser.parse_args(argv[1:])
@@ -40,6 +46,9 @@ def main(argv: list[str]):
40
46
  output_file = args.output_file
41
47
  force = args.force
42
48
 
49
+ p = Path(output_file)
50
+ output_directory, file_name = str(p.parent), str(p.name)
51
+
43
52
  if beamline:
44
53
  os.environ["BEAMLINE"] = beamline
45
54
  else:
@@ -49,36 +58,43 @@ def main(argv: list[str]):
49
58
  sys.stderr.write("BEAMLINE not set and --beamline not specified\n")
50
59
  return 1
51
60
 
52
- if Path(output_file).exists() and not force:
61
+ if Path(f"{output_directory}/{file_name}").exists() and not force:
53
62
  sys.stderr.write(
54
- f"Output file {output_file} already exists and --force not specified."
63
+ f"Output file {output_directory}/{file_name} already exists and --force not specified."
55
64
  )
56
65
  return 1
57
66
 
58
- _save_panda(beamline, device_name, output_file)
67
+ _save_panda(beamline, device_name, output_directory, file_name)
59
68
 
60
69
  print("Done.")
61
70
  return 0
62
71
 
63
72
 
64
- def _save_panda(beamline, device_name, output_file):
73
+ def _save_panda(beamline, device_name, output_directory, file_name):
65
74
  RE = RunEngine()
66
75
  print("Creating devices...")
67
76
  module_name = module_name_for_beamline(beamline)
68
77
  try:
69
- devices = make_device(f"dodal.beamlines.{module_name}", device_name)
78
+ devices = make_device(
79
+ f"dodal.beamlines.{module_name}", device_name, connect_immediately=True
80
+ )
70
81
  except Exception as error:
71
82
  sys.stderr.write(f"Couldn't create device {device_name}: {error}\n")
72
83
  sys.exit(1)
73
84
 
74
85
  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)
86
+ print(
87
+ f"Saving to {output_directory}/{file_name} from {device_name} on {beamline}..."
88
+ )
89
+ _save_panda_to_yaml(RE, cast(Device, panda), file_name, output_directory)
77
90
 
78
91
 
79
- def _save_panda_to_file(RE: RunEngine, panda: Device, path: str):
92
+ def _save_panda_to_yaml(
93
+ RE: RunEngine, panda: Device, file_name: str, output_directory: str
94
+ ):
80
95
  def save_to_file():
81
- yield from save_device(panda, path, sorter=phase_sorter)
96
+ provider = YamlSettingsProvider(output_directory)
97
+ yield from store_settings(provider, file_name, panda)
82
98
 
83
99
  RE(save_to_file())
84
100
 
dodal/utils.py CHANGED
@@ -16,6 +16,7 @@ from typing import (
16
16
  Any,
17
17
  Generic,
18
18
  Protocol,
19
+ TypeAlias,
19
20
  TypeGuard,
20
21
  TypeVar,
21
22
  runtime_checkable,
@@ -43,12 +44,6 @@ from ophyd_async.core import Device as OphydV2Device
43
44
 
44
45
  import dodal.log
45
46
 
46
- try:
47
- from typing import TypeAlias
48
- except ImportError:
49
- from typing import TypeAlias
50
-
51
-
52
47
  #: Protocols defining interface to hardware
53
48
  BLUESKY_PROTOCOLS = [
54
49
  Checkable,
@@ -102,7 +97,7 @@ class BeamlinePrefix:
102
97
 
103
98
 
104
99
  T = TypeVar("T", bound=AnyDevice)
105
- D = TypeVar("D", bound=OphydV2Device)
100
+
106
101
  SkipType = bool | Callable[[], bool]
107
102
 
108
103
 
@@ -119,16 +114,16 @@ def skip_device(precondition=lambda: True):
119
114
  return decorator
120
115
 
121
116
 
122
- class DeviceInitializationController(Generic[D]):
117
+ class DeviceInitializationController(Generic[T]):
123
118
  def __init__(
124
119
  self,
125
- factory: Callable[[], D],
120
+ factory: Callable[[], T],
126
121
  use_factory_name: bool,
127
122
  timeout: float,
128
123
  mock: bool,
129
124
  skip: SkipType,
130
125
  ):
131
- self._factory: Callable[[], D] = functools.cache(factory)
126
+ self._factory: Callable[..., T] = functools.cache(factory)
132
127
  self._use_factory_name = use_factory_name
133
128
  self._timeout = timeout
134
129
  self._mock = mock
@@ -153,13 +148,15 @@ class DeviceInitializationController(Generic[D]):
153
148
  name: str | None = None,
154
149
  connection_timeout: float | None = None,
155
150
  mock: bool | None = None,
156
- ) -> D:
151
+ **kwargs,
152
+ ) -> T:
157
153
  """Returns an instance of the Device the wrapped factory produces: the same
158
154
  instance will be returned if this method is called multiple times, and arguments
159
155
  may be passed to override this Controller's configuration.
160
156
  Once the device is connected, the value of mock must be consistent, or connect
161
157
  must be False.
162
158
 
159
+ Additional keyword arguments will be passed through to the wrapped factory function.
163
160
 
164
161
  Args:
165
162
  connect_immediately (bool, default False): whether to call connect on the
@@ -182,19 +179,36 @@ class DeviceInitializationController(Generic[D]):
182
179
  connect is called on the Device.
183
180
 
184
181
  Returns:
185
- D: a singleton instance of the Device class returned by the wrapped factory.
182
+ T: a singleton instance of the Device class returned by the wrapped factory.
183
+
184
+ Raises:
185
+ RuntimeError: If the device factory was invoked again with different
186
+ keyword arguments, without previously invoking cache_clear()
186
187
  """
187
- device = self._factory()
188
+ is_v2_device = is_v2_device_factory(self._factory)
189
+ is_mock = mock if mock is not None else self._mock
190
+ if is_v2_device:
191
+ device: T = self._factory(**kwargs)
192
+ else:
193
+ device: T = self._factory(mock=is_mock, **kwargs)
194
+
195
+ if self._factory.cache_info().currsize > 1: # type: ignore
196
+ raise RuntimeError(
197
+ f"Device factory method called multiple times with different parameters: "
198
+ f"{self.__name__}" # type: ignore
199
+ )
188
200
 
189
201
  if connect_immediately:
190
- call_in_bluesky_event_loop(
191
- device.connect(
192
- timeout=connection_timeout
193
- if connection_timeout is not None
194
- else self._timeout,
195
- mock=mock if mock is not None else self._mock,
196
- )
202
+ timeout = (
203
+ connection_timeout if connection_timeout is not None else self._timeout
197
204
  )
205
+ if is_v2_device:
206
+ call_in_bluesky_event_loop(
207
+ device.connect(timeout=timeout, mock=is_mock)
208
+ )
209
+ else:
210
+ assert is_v1_device_type(type(device))
211
+ device.wait_for_connection(timeout=timeout) # type: ignore
198
212
 
199
213
  if name:
200
214
  device.set_name(name)
@@ -410,7 +424,27 @@ def is_any_device_factory(func: Callable) -> TypeGuard[AnyDeviceFactory]:
410
424
 
411
425
 
412
426
  def is_v2_device_type(obj: type[Any]) -> bool:
413
- return inspect.isclass(obj) and isinstance(obj, OphydV2Device)
427
+ non_parameterized_class = None
428
+ if obj != inspect.Signature.empty:
429
+ if inspect.isclass(obj):
430
+ non_parameterized_class = obj
431
+ elif hasattr(obj, "__origin__"):
432
+ # typing._GenericAlias is the same as types.GenericAlias, maybe?
433
+ # This is all very badly documented and possibly prone to change in future versions of Python
434
+ non_parameterized_class = obj.__origin__
435
+ if non_parameterized_class:
436
+ try:
437
+ return non_parameterized_class and issubclass(
438
+ non_parameterized_class, OphydV2Device
439
+ )
440
+ except TypeError:
441
+ # Python 3.10 will return inspect.isclass(t) == True but then
442
+ # raise TypeError: issubclass() arg 1 must be a class
443
+ # when inspecting device_factory decorator function itself
444
+ # Later versions of Python seem not to be affected
445
+ pass
446
+
447
+ return False
414
448
 
415
449
 
416
450
  def is_v1_device_type(obj: type[Any]) -> bool: