dls-dodal 1.65.0__py3-none-any.whl → 1.67.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.65.0.dist-info → dls_dodal-1.67.0.dist-info}/METADATA +3 -4
  2. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/RECORD +82 -66
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/aithre.py +21 -2
  5. dodal/beamlines/i03.py +102 -198
  6. dodal/beamlines/i04.py +40 -4
  7. dodal/beamlines/i05.py +28 -1
  8. dodal/beamlines/i06.py +62 -0
  9. dodal/beamlines/i07.py +20 -0
  10. dodal/beamlines/i09_1.py +32 -3
  11. dodal/beamlines/i09_2.py +57 -2
  12. dodal/beamlines/i10_optics.py +46 -17
  13. dodal/beamlines/i17.py +7 -3
  14. dodal/beamlines/i18.py +3 -3
  15. dodal/beamlines/i19_1.py +26 -14
  16. dodal/beamlines/i19_2.py +49 -38
  17. dodal/beamlines/i21.py +2 -2
  18. dodal/beamlines/i22.py +19 -4
  19. dodal/beamlines/p38.py +3 -3
  20. dodal/beamlines/training_rig.py +0 -16
  21. dodal/cli.py +26 -12
  22. dodal/common/coordination.py +3 -2
  23. dodal/device_manager.py +604 -0
  24. dodal/devices/aithre_lasershaping/goniometer.py +26 -9
  25. dodal/devices/aperturescatterguard.py +3 -2
  26. dodal/devices/areadetector/plugins/mjpg.py +10 -3
  27. dodal/devices/beamsize/__init__.py +0 -0
  28. dodal/devices/beamsize/beamsize.py +6 -0
  29. dodal/devices/cryostream.py +28 -57
  30. dodal/devices/detector/det_resolution.py +4 -2
  31. dodal/devices/eiger.py +26 -18
  32. dodal/devices/fast_grid_scan.py +14 -2
  33. dodal/devices/i03/beamsize.py +35 -0
  34. dodal/devices/i03/constants.py +7 -0
  35. dodal/devices/i03/undulator_dcm.py +2 -2
  36. dodal/devices/i04/beamsize.py +45 -0
  37. dodal/devices/i04/max_pixel.py +38 -0
  38. dodal/devices/i04/murko_results.py +36 -26
  39. dodal/devices/i04/transfocator.py +23 -29
  40. dodal/devices/i07/id.py +38 -0
  41. dodal/devices/i09_1_shared/__init__.py +13 -2
  42. dodal/devices/i09_1_shared/hard_energy.py +112 -0
  43. dodal/devices/i09_1_shared/hard_undulator_functions.py +85 -21
  44. dodal/devices/i09_2_shared/__init__.py +0 -0
  45. dodal/devices/i09_2_shared/i09_apple2.py +86 -0
  46. dodal/devices/i10/i10_apple2.py +39 -331
  47. dodal/devices/i17/i17_apple2.py +37 -22
  48. dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
  49. dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
  50. dodal/devices/i19/access_controlled/shutter.py +2 -4
  51. dodal/devices/insertion_device/__init__.py +0 -0
  52. dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +122 -69
  53. dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
  54. dodal/devices/insertion_device/lookup_table_models.py +287 -0
  55. dodal/devices/ipin.py +20 -2
  56. dodal/devices/motors.py +33 -3
  57. dodal/devices/mx_phase1/beamstop.py +31 -12
  58. dodal/devices/oav/oav_calculations.py +9 -4
  59. dodal/devices/oav/oav_detector.py +65 -7
  60. dodal/devices/oav/oav_parameters.py +3 -1
  61. dodal/devices/oav/oav_to_redis_forwarder.py +18 -15
  62. dodal/devices/oav/pin_image_recognition/__init__.py +5 -1
  63. dodal/devices/oav/pin_image_recognition/utils.py +23 -1
  64. dodal/devices/oav/snapshots/snapshot_with_grid.py +8 -2
  65. dodal/devices/oav/utils.py +16 -6
  66. dodal/devices/robot.py +33 -18
  67. dodal/devices/scintillator.py +36 -14
  68. dodal/devices/smargon.py +2 -3
  69. dodal/devices/thawer.py +7 -45
  70. dodal/devices/undulator.py +152 -68
  71. dodal/plans/__init__.py +1 -1
  72. dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
  73. dodal/plans/load_panda_yaml.py +9 -0
  74. dodal/plans/verify_undulator_gap.py +2 -2
  75. dodal/testing/fixtures/devices/__init__.py +0 -0
  76. dodal/testing/fixtures/devices/apple2.py +78 -0
  77. dodal/utils.py +6 -3
  78. dodal/beamline_specific_utils/i03.py +0 -17
  79. dodal/testing/__init__.py +0 -3
  80. dodal/testing/setup.py +0 -67
  81. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/WHEEL +0 -0
  82. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/entry_points.txt +0 -0
  83. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/licenses/LICENSE +0 -0
  84. {dls_dodal-1.65.0.dist-info → dls_dodal-1.67.0.dist-info}/top_level.txt +0 -0
  85. /dodal/plans/{scanspec.py → spec_path.py} +0 -0
@@ -1,4 +1,5 @@
1
1
  import os
2
+ from abc import ABC, abstractmethod
2
3
 
3
4
  import numpy as np
4
5
  from bluesky.protocols import Locatable, Location, Movable
@@ -39,111 +40,177 @@ def _get_gap_for_energy(
39
40
  )
40
41
 
41
42
 
42
- class Undulator(StandardReadable, Movable[float]):
43
+ class BaseUndulator(StandardReadable, Movable[float], ABC):
43
44
  """
44
- An Undulator-type insertion device, used to control photon emission at a given
45
- beam energy.
45
+ Base class for undulator devices providing gap control and access management.
46
+ This class expects target gap value [mm] passed in set method.
46
47
  """
47
48
 
48
49
  def __init__(
49
50
  self,
50
51
  prefix: str,
51
- id_gap_lookup_table_path: str = os.devnull,
52
- name: str = "",
53
52
  poles: int | None = None,
54
53
  length: float | None = None,
54
+ undulator_period: int | None = None,
55
55
  baton: Baton | None = None,
56
+ name: str = "",
56
57
  ) -> None:
57
- """Constructor
58
-
58
+ """
59
59
  Args:
60
60
  prefix: PV prefix
61
- poles (int): Number of magnetic poles built into the undulator
62
- length (float): Length of the undulator in meters
61
+ poles (int, optional): Number of magnetic poles built into the undulator
62
+ length (float, optional): Length of the undulator in meters
63
+ undulator_period(int, optional): Undulator period
64
+ baton (optional): Baton object if provided.
63
65
  name (str, optional): Name for device. Defaults to "".
64
66
  """
65
-
66
67
  self.baton_ref = Reference(baton) if baton else None
67
- self.id_gap_lookup_table_path = id_gap_lookup_table_path
68
+
68
69
  with self.add_children_as_readables():
70
+ self.gap_access = epics_signal_r(EnabledDisabledUpper, prefix + "IDBLENA")
69
71
  self.gap_motor = Motor(prefix + "BLGAPMTR")
70
72
  self.current_gap = epics_signal_r(float, prefix + "CURRGAPD")
71
- self.gap_access = epics_signal_r(EnabledDisabledUpper, prefix + "IDBLENA")
72
73
 
73
74
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
74
75
  self.gap_discrepancy_tolerance_mm, _ = soft_signal_r_and_setter(
75
76
  float,
76
77
  initial_value=UNDULATOR_DISCREPANCY_THRESHOLD_MM,
77
78
  )
78
- if poles is not None:
79
- self.poles, _ = soft_signal_r_and_setter(
80
- int,
81
- initial_value=poles,
82
- )
83
- else:
84
- self.poles = None
85
-
86
- if length is not None:
87
- self.length, _ = soft_signal_r_and_setter(
88
- float,
89
- initial_value=length,
90
- )
91
- else:
92
- self.length = None
93
-
94
- super().__init__(name)
79
+ self.poles = self._make_signal_if_not_none(poles, int)
80
+ self.length = self._make_signal_if_not_none(length, float)
81
+ self.undulator_period = self._make_signal_if_not_none(undulator_period, int)
95
82
 
83
+ super().__init__(name=name)
84
+
85
+ def _make_signal_if_not_none(self, initial_value, type):
86
+ if initial_value is None:
87
+ return None
88
+ signal, _ = soft_signal_r_and_setter(type, initial_value=initial_value)
89
+ return signal
90
+
91
+ @abstractmethod
96
92
  @AsyncStatus.wrap
97
- async def set(self, value: float):
93
+ async def set(self, value: float) -> None:
98
94
  """
99
- Set the undulator gap to a given energy in keV
95
+ Move undulator to a given position.
96
+ Abstract method - must be implemented by subclasses.
100
97
 
101
98
  Args:
102
- value: energy in keV
99
+ value: target position - units depend on implementation
103
100
  """
104
- await self._set_undulator_gap(value)
101
+ ...
105
102
 
106
- async def raise_if_not_enabled(self):
107
- access_level = await self.gap_access.get_value()
108
- commissioning_mode = await self._is_commissioning_mode_enabled()
109
- if access_level is EnabledDisabledUpper.DISABLED and not commissioning_mode:
110
- raise AccessError("Undulator gap access is disabled. Contact Control Room")
103
+ async def _set_gap(self, value: float) -> None:
104
+ """
105
+ Set the undulator gap to a given value in mm.
106
+
107
+ Args:
108
+ value: gap in mm
109
+ """
110
+ await self.raise_if_not_enabled() # Check access
111
+ if await self._check_gap_within_threshold(value):
112
+ LOGGER.debug(
113
+ "Gap is already in the correct place, no need to ask it to move"
114
+ )
115
+ return
111
116
 
112
- async def _set_undulator_gap(self, energy_kev: float) -> None:
113
- await self.raise_if_not_enabled()
114
- target_gap = await self._get_gap_to_match_energy(energy_kev)
115
117
  LOGGER.info(
116
- f"Setting undulator gap to {target_gap:.3f}mm based on {energy_kev:.2f}kev"
118
+ f"Undulator gap mismatch. Moving gap to nominal value, {value:.3f}mm"
117
119
  )
120
+ commissioning_mode = await self._is_commissioning_mode_enabled()
121
+ if not commissioning_mode:
122
+ # Only move if the gap is sufficiently different to the value from the
123
+ # DCM lookup table AND we're not in commissioning mode
124
+ await self.gap_motor.set(
125
+ value,
126
+ timeout=STATUS_TIMEOUT_S,
127
+ )
128
+ else:
129
+ LOGGER.warning("In test mode, not moving ID gap")
118
130
 
119
- # Check if undulator gap is close enough to the value from the DCM
131
+ async def _check_gap_within_threshold(self, target_gap: float) -> bool:
132
+ """
133
+ Check if the undulator gap is within the acceptable threshold of the target gap.
134
+
135
+ Args:
136
+ target_gap: target gap in mm
137
+ Returns:
138
+ True if the gap is within the threshold, False otherwise
139
+ """
120
140
  current_gap = await self.current_gap.get_value()
121
141
  tolerance = await self.gap_discrepancy_tolerance_mm.get_value()
122
- difference = abs(target_gap - current_gap)
123
- if difference > tolerance:
124
- LOGGER.info(
125
- f"Undulator gap mismatch. {difference:.3f}mm is outside tolerance.\
126
- Moving gap to nominal value, {target_gap:.3f}mm"
127
- )
128
- commissioning_mode = await self._is_commissioning_mode_enabled()
129
- if not commissioning_mode:
130
- # Only move if the gap is sufficiently different to the value from the
131
- # DCM lookup table AND we're not in commissioning mode
132
- await self.gap_motor.set(
133
- target_gap,
134
- timeout=STATUS_TIMEOUT_S,
135
- )
136
- else:
137
- LOGGER.warning("In test mode, not moving ID gap")
138
- else:
139
- LOGGER.debug(
140
- "Gap is already in the correct place for the new energy value "
141
- f"{energy_kev}, no need to ask it to move"
142
- )
142
+ return abs(target_gap - current_gap) <= tolerance
143
143
 
144
- async def _is_commissioning_mode_enabled(self):
144
+ async def _is_commissioning_mode_enabled(self) -> bool | None:
145
+ """
146
+ Asynchronously checks if commissioning mode is enabled via the baton reference.
147
+ """
145
148
  return self.baton_ref and await self.baton_ref().commissioning.get_value()
146
149
 
150
+ async def raise_if_not_enabled(self) -> AccessError | None:
151
+ """
152
+ Asynchronously raises AccessError if gap access is disabled and not in commissioning mode.
153
+ """
154
+ access_level = await self.gap_access.get_value()
155
+ commissioning_mode = await self._is_commissioning_mode_enabled()
156
+ if access_level is EnabledDisabledUpper.DISABLED and not commissioning_mode:
157
+ raise AccessError("Undulator gap access is disabled. Contact Control Room")
158
+
159
+
160
+ class UndulatorInKeV(BaseUndulator):
161
+ """
162
+ An Undulator-type insertion device, used to control photon emission at a given beam energy.
163
+ This class expects energy [keV] passed in set method and does conversion to gap
164
+ internally, for which it requires path to lookup table file in constructor.
165
+ """
166
+
167
+ def __init__(
168
+ self,
169
+ prefix: str,
170
+ id_gap_lookup_table_path: str = os.devnull,
171
+ poles: int | None = None,
172
+ length: float | None = None,
173
+ undulator_period: int | None = None,
174
+ baton: Baton | None = None,
175
+ name: str = "",
176
+ ) -> None:
177
+ """Constructor
178
+
179
+ Args:
180
+ prefix: PV prefix
181
+ id_gap_lookup_table_path (str): Path to a lookup table file
182
+ poles (int, optional): Number of magnetic poles built into the undulator
183
+ length (float, optional): Length of the undulator in meters
184
+ undulator_period(int, optional): Undulator period
185
+ baton (optional): Baton object if provided.
186
+ name (str, optional): Name for device. Defaults to "".
187
+ """
188
+
189
+ self.id_gap_lookup_table_path = id_gap_lookup_table_path
190
+ super().__init__(
191
+ prefix=prefix,
192
+ poles=poles,
193
+ length=length,
194
+ undulator_period=undulator_period,
195
+ baton=baton,
196
+ name=name,
197
+ )
198
+
199
+ @AsyncStatus.wrap
200
+ async def set(self, value: float):
201
+ """
202
+ Check conditions and Set undulator gap to a given energy in keV
203
+
204
+ Args:
205
+ value: energy in keV
206
+ """
207
+ # Convert energy in keV to gap in mm first
208
+ target_gap = await self._get_gap_to_match_energy(value)
209
+ LOGGER.info(
210
+ f"Setting undulator gap to {target_gap:.3f}mm based on {value:.2f}kev"
211
+ )
212
+ await self._set_gap(target_gap)
213
+
147
214
  async def _get_gap_to_match_energy(self, energy_kev: float) -> float:
148
215
  """
149
216
  get a 2d np.array from lookup table that
@@ -160,6 +227,23 @@ class Undulator(StandardReadable, Movable[float]):
160
227
  )
161
228
 
162
229
 
230
+ class UndulatorInMm(BaseUndulator):
231
+ """
232
+ An Undulator-type insertion device, used to control photon emission.
233
+ This class expects gap [mm] passed in set method.
234
+ """
235
+
236
+ @AsyncStatus.wrap
237
+ async def set(self, value: float):
238
+ """
239
+ Check conditions and Set undulator gap to a given value in mm
240
+
241
+ Args:
242
+ value: value in mm
243
+ """
244
+ await self._set_gap(value)
245
+
246
+
163
247
  class UndulatorOrder(StandardReadable, Locatable[int]):
164
248
  """
165
249
  Represents the order of an undulator device. Allows setting and locating the order.
@@ -171,17 +255,17 @@ class UndulatorOrder(StandardReadable, Locatable[int]):
171
255
  name: Name for device. Defaults to ""
172
256
  """
173
257
  with self.add_children_as_readables():
174
- self._value = soft_signal_rw(int, initial_value=3)
258
+ self.value = soft_signal_rw(int, initial_value=3)
175
259
  super().__init__(name=name)
176
260
 
177
261
  @AsyncStatus.wrap
178
262
  async def set(self, value: int) -> None:
179
263
  if (value >= 0) and isinstance(value, int):
180
- await self._value.set(value)
264
+ await self.value.set(value)
181
265
  else:
182
266
  raise ValueError(
183
267
  f"Undulator order must be a positive integer. Requested value: {value}"
184
268
  )
185
269
 
186
270
  async def locate(self) -> Location[int]:
187
- return await self._value.locate()
271
+ return await self.value.locate()
dodal/plans/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- from .scanspec import spec_scan
1
+ from .spec_path import spec_scan
2
2
  from .wrapped import count
3
3
 
4
4
  __all__ = ["count", "spec_scan"]
@@ -12,7 +12,7 @@ from ophyd_async.core import (
12
12
  )
13
13
  from ophyd_async.fastcs.eiger import EigerDetector
14
14
 
15
- from dodal.beamlines.i03 import fastcs_eiger, set_path_provider
15
+ from dodal.beamlines.i03 import fastcs_eiger
16
16
  from dodal.devices.detector import DetectorParams
17
17
  from dodal.log import LOGGER, do_default_logging_setup
18
18
 
@@ -163,9 +163,7 @@ if __name__ == "__main__":
163
163
  PurePath("/dls/i03/data/2025/cm40607-2/test_new_eiger/"),
164
164
  )
165
165
 
166
- set_path_provider(path_provider)
167
-
168
- eiger = fastcs_eiger(connect_immediately=True)
166
+ eiger = fastcs_eiger.build(connect_immediately=True, path_provider=path_provider)
169
167
  run_engine(
170
168
  configure_arm_trigger_and_disarm_detector(
171
169
  eiger=eiger,
@@ -0,0 +1,9 @@
1
+ from ophyd_async.core import YamlSettingsProvider
2
+ from ophyd_async.fastcs.panda import HDFPanda
3
+ from ophyd_async.plan_stubs import apply_panda_settings, retrieve_settings
4
+
5
+
6
+ def load_panda_from_yaml(yaml_directory: str, yaml_file_name: str, panda: HDFPanda):
7
+ provider = YamlSettingsProvider(yaml_directory)
8
+ settings = yield from retrieve_settings(provider, yaml_file_name, panda)
9
+ yield from apply_panda_settings(settings)
@@ -3,12 +3,12 @@ from typing import Protocol, runtime_checkable
3
3
  from bluesky import plan_stubs as bps
4
4
 
5
5
  from dodal.devices.common_dcm import DoubleCrystalMonochromatorBase
6
- from dodal.devices.undulator import Undulator
6
+ from dodal.devices.undulator import UndulatorInKeV
7
7
 
8
8
 
9
9
  @runtime_checkable
10
10
  class CheckUndulatorDevices(Protocol):
11
- undulator: Undulator
11
+ undulator: UndulatorInKeV
12
12
  dcm: DoubleCrystalMonochromatorBase
13
13
 
14
14
 
File without changes
@@ -0,0 +1,78 @@
1
+ from unittest.mock import MagicMock
2
+
3
+ import pytest
4
+ from daq_config_server.client import ConfigServer
5
+ from ophyd_async.core import (
6
+ init_devices,
7
+ set_mock_value,
8
+ )
9
+
10
+ from dodal.devices.insertion_device.apple2_undulator import (
11
+ EnabledDisabledUpper,
12
+ UndulatorGap,
13
+ UndulatorGateStatus,
14
+ UndulatorJawPhase,
15
+ UndulatorPhaseAxes,
16
+ )
17
+
18
+
19
+ @pytest.fixture
20
+ def mock_config_client() -> ConfigServer:
21
+ mock_config_client = ConfigServer()
22
+
23
+ mock_config_client.get_file_contents = MagicMock(spec=["get_file_contents"])
24
+
25
+ def my_side_effect(file_path, reset_cached_result) -> str:
26
+ assert reset_cached_result is True
27
+ with open(file_path) as f:
28
+ return f.read()
29
+
30
+ mock_config_client.get_file_contents.side_effect = my_side_effect
31
+ return mock_config_client
32
+
33
+
34
+ @pytest.fixture
35
+ async def mock_id_gap(prefix: str = "BLXX-EA-DET-007:") -> UndulatorGap:
36
+ async with init_devices(mock=True):
37
+ mock_id_gap = UndulatorGap(prefix, "mock_id_gap")
38
+ assert mock_id_gap.name == "mock_id_gap"
39
+ set_mock_value(mock_id_gap.gate, UndulatorGateStatus.CLOSE)
40
+ set_mock_value(mock_id_gap.velocity, 1)
41
+ set_mock_value(mock_id_gap.user_readback, 1)
42
+ set_mock_value(mock_id_gap.user_setpoint, "1")
43
+ set_mock_value(mock_id_gap.status, EnabledDisabledUpper.ENABLED)
44
+ return mock_id_gap
45
+
46
+
47
+ @pytest.fixture
48
+ async def mock_phase_axes(prefix: str = "BLXX-EA-DET-007:") -> UndulatorPhaseAxes:
49
+ async with init_devices(mock=True):
50
+ mock_phase_axes = UndulatorPhaseAxes(
51
+ prefix=prefix,
52
+ top_outer="RPQ1",
53
+ top_inner="RPQ2",
54
+ btm_outer="RPQ3",
55
+ btm_inner="RPQ4",
56
+ )
57
+ assert mock_phase_axes.name == "mock_phase_axes"
58
+ set_mock_value(mock_phase_axes.gate, UndulatorGateStatus.CLOSE)
59
+ set_mock_value(mock_phase_axes.top_outer.velocity, 2)
60
+ set_mock_value(mock_phase_axes.top_inner.velocity, 2)
61
+ set_mock_value(mock_phase_axes.btm_outer.velocity, 2)
62
+ set_mock_value(mock_phase_axes.btm_inner.velocity, 2)
63
+ set_mock_value(mock_phase_axes.status, EnabledDisabledUpper.ENABLED)
64
+ return mock_phase_axes
65
+
66
+
67
+ @pytest.fixture
68
+ async def mock_jaw_phase(prefix: str = "BLXX-EA-DET-007:") -> UndulatorJawPhase:
69
+ async with init_devices(mock=True):
70
+ mock_jaw_phase = UndulatorJawPhase(
71
+ prefix=prefix, move_pv="RPQ1", jaw_phase="JAW"
72
+ )
73
+ set_mock_value(mock_jaw_phase.gate, UndulatorGateStatus.CLOSE)
74
+ set_mock_value(mock_jaw_phase.jaw_phase.velocity, 2)
75
+ set_mock_value(mock_jaw_phase.jaw_phase.user_readback, 0)
76
+ set_mock_value(mock_jaw_phase.jaw_phase.user_setpoint_readback, 0)
77
+ set_mock_value(mock_jaw_phase.status, EnabledDisabledUpper.ENABLED)
78
+ return mock_jaw_phase
dodal/utils.py CHANGED
@@ -11,7 +11,7 @@ from functools import update_wrapper, wraps
11
11
  from importlib import import_module
12
12
  from inspect import signature
13
13
  from os import environ
14
- from types import ModuleType
14
+ from types import FunctionType, ModuleType
15
15
  from typing import (
16
16
  Any,
17
17
  Generic,
@@ -240,7 +240,8 @@ def make_device(
240
240
  def make_all_devices(
241
241
  module: str | ModuleType | None = None, include_skipped: bool = False, **kwargs
242
242
  ) -> tuple[dict[str, AnyDevice], dict[str, Exception]]:
243
- """Makes all devices in the given beamline module.
243
+ """Makes all devices in the given beamline module, for those modules using device factories
244
+ as opposed to the DeviceManager.
244
245
 
245
246
  In cases of device interdependencies it ensures a device is created before any which
246
247
  depend on it.
@@ -413,7 +414,9 @@ def is_v2_device_factory(func: Callable) -> TypeGuard[V2DeviceFactory]:
413
414
 
414
415
 
415
416
  def is_any_device_factory(func: Callable) -> TypeGuard[AnyDeviceFactory]:
416
- return is_v1_device_factory(func) or is_v2_device_factory(func)
417
+ return isinstance(func, FunctionType | DeviceInitializationController) and (
418
+ is_v1_device_factory(func) or is_v2_device_factory(func)
419
+ )
417
420
 
418
421
 
419
422
  def is_v2_device_type(obj: type[Any]) -> bool:
@@ -1,17 +0,0 @@
1
- from dataclasses import dataclass
2
-
3
- I03_BEAM_HEIGHT_UM = 20.0
4
- I03_BEAM_WIDTH_UM = 80.0
5
-
6
-
7
- @dataclass
8
- class BeamSize:
9
- x_um: float | None
10
- y_um: float | None
11
-
12
-
13
- def beam_size_from_aperture(aperture_size: float | None):
14
- return BeamSize(
15
- min(aperture_size, I03_BEAM_WIDTH_UM) if aperture_size else None,
16
- I03_BEAM_HEIGHT_UM if aperture_size else None,
17
- )
dodal/testing/__init__.py DELETED
@@ -1,3 +0,0 @@
1
- from .setup import patch_all_motors, patch_motor
2
-
3
- __all__ = ["patch_motor", "patch_all_motors"]
dodal/testing/setup.py DELETED
@@ -1,67 +0,0 @@
1
- from contextlib import ExitStack
2
-
3
- from ophyd_async.core import Device
4
- from ophyd_async.epics.motor import Motor
5
- from ophyd_async.testing import (
6
- callback_on_mock_put,
7
- set_mock_value,
8
- )
9
-
10
-
11
- def patch_motor(
12
- motor: Motor,
13
- initial_position: float = 0,
14
- deadband: float = 0.001,
15
- velocity: float = 3,
16
- max_velocity: float = 5,
17
- low_limit_travel: float = float("-inf"),
18
- high_limit_travel: float = float("inf"),
19
- ):
20
- """
21
- Patch a mock motor with sensible default values so that it can still be used in
22
- tests and plans without running into errors as default values are zero.
23
-
24
- Parameters:
25
- motor: The mock motor to set mock values with.
26
- initial_position: The default initial position of the motor to be set.
27
- deadband: The tolerance between readback value and demand setpoint which the
28
- motor is considered at position.
29
- velocity: Requested move speed when the mock motor moves.
30
- max_velocity: The maximum allowable velocity that can be set for the motor.
31
- low_limit_travel: The lower limit that the motor can move to.
32
- high_limit_travel: The higher limit that the motor can move to.
33
- """
34
- set_mock_value(motor.user_setpoint, initial_position)
35
- set_mock_value(motor.user_readback, initial_position)
36
- set_mock_value(motor.deadband, deadband)
37
- set_mock_value(motor.motor_done_move, 1)
38
- set_mock_value(motor.velocity, velocity)
39
- set_mock_value(motor.max_velocity, max_velocity)
40
- set_mock_value(motor.low_limit_travel, low_limit_travel)
41
- set_mock_value(motor.high_limit_travel, high_limit_travel)
42
- return callback_on_mock_put(
43
- motor.user_setpoint,
44
- lambda pos, *args, **kwargs: set_mock_value(motor.user_readback, pos),
45
- )
46
-
47
-
48
- def patch_all_motors(parent_device: Device):
49
- """
50
- Check all children of a device and patch any motors with mock values.
51
-
52
- Parameters:
53
- parent_device: The device that hold motor(s) as children.
54
- """
55
- motors = []
56
-
57
- def recursively_find_motors(device: Device):
58
- for _, child_device in device.children():
59
- if isinstance(child_device, Motor):
60
- motors.append(child_device)
61
- recursively_find_motors(child_device)
62
-
63
- recursively_find_motors(parent_device)
64
- motor_patch_stack = ExitStack()
65
- for motor in motors:
66
- motor_patch_stack.enter_context(patch_motor(motor))
67
- return motor_patch_stack
File without changes