dls-dodal 1.55.1__py3-none-any.whl → 1.56.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 (98) hide show
  1. {dls_dodal-1.55.1.dist-info → dls_dodal-1.56.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.55.1.dist-info → dls_dodal-1.56.0.dist-info}/RECORD +97 -83
  3. dodal/_version.py +16 -3
  4. dodal/beamlines/b01_1.py +6 -1
  5. dodal/beamlines/b07.py +2 -1
  6. dodal/beamlines/b07_1.py +2 -1
  7. dodal/beamlines/b21.py +4 -24
  8. dodal/beamlines/i03.py +53 -53
  9. dodal/beamlines/i04.py +16 -38
  10. dodal/beamlines/i09.py +3 -2
  11. dodal/beamlines/i09_1.py +2 -1
  12. dodal/beamlines/i11.py +143 -0
  13. dodal/beamlines/i19_1.py +1 -0
  14. dodal/beamlines/i19_2.py +7 -0
  15. dodal/beamlines/i22.py +2 -2
  16. dodal/beamlines/i23.py +3 -3
  17. dodal/beamlines/i24.py +6 -14
  18. dodal/beamlines/p38.py +1 -0
  19. dodal/beamlines/p60.py +3 -2
  20. dodal/cli.py +11 -1
  21. dodal/common/__init__.py +4 -0
  22. dodal/common/beamlines/beamline_parameters.py +1 -1
  23. dodal/common/beamlines/beamline_utils.py +5 -1
  24. dodal/common/enums.py +19 -0
  25. dodal/common/watcher_utils.py +83 -0
  26. dodal/devices/aithre_lasershaping/laser_robot.py +4 -9
  27. dodal/devices/aperturescatterguard.py +52 -12
  28. dodal/devices/apple2_undulator.py +0 -1
  29. dodal/devices/b16/detector.py +1 -10
  30. dodal/devices/backlight.py +8 -20
  31. dodal/devices/bimorph_mirror.py +4 -7
  32. dodal/devices/collimation_table.py +36 -0
  33. dodal/devices/controllers.py +21 -0
  34. dodal/devices/cryostream.py +97 -7
  35. dodal/devices/current_amplifiers/femto.py +1 -1
  36. dodal/devices/detector/detector_motion.py +1 -7
  37. dodal/devices/eiger.py +22 -8
  38. dodal/devices/eiger_odin.py +2 -0
  39. dodal/devices/electron_analyser/__init__.py +2 -1
  40. dodal/devices/electron_analyser/abstract/__init__.py +0 -1
  41. dodal/devices/electron_analyser/abstract/base_detector.py +3 -25
  42. dodal/devices/electron_analyser/abstract/base_driver_io.py +18 -9
  43. dodal/devices/electron_analyser/abstract/base_region.py +34 -3
  44. dodal/devices/electron_analyser/detector.py +24 -0
  45. dodal/devices/electron_analyser/enums.py +5 -0
  46. dodal/devices/electron_analyser/specs/detector.py +2 -1
  47. dodal/devices/electron_analyser/specs/driver_io.py +21 -26
  48. dodal/devices/electron_analyser/specs/region.py +1 -1
  49. dodal/devices/electron_analyser/util.py +20 -0
  50. dodal/devices/electron_analyser/vgscienta/__init__.py +3 -3
  51. dodal/devices/electron_analyser/vgscienta/detector.py +2 -1
  52. dodal/devices/electron_analyser/vgscienta/driver_io.py +24 -32
  53. dodal/devices/electron_analyser/vgscienta/enums.py +0 -8
  54. dodal/devices/electron_analyser/vgscienta/region.py +2 -31
  55. dodal/devices/eurotherm.py +126 -0
  56. dodal/devices/fluorescence_detector_motion.py +3 -10
  57. dodal/devices/focusing_mirror.py +1 -1
  58. dodal/devices/i03/undulator_dcm.py +0 -1
  59. dodal/devices/i09/enums.py +8 -8
  60. dodal/devices/i10/diagnostics.py +4 -4
  61. dodal/devices/i10/i10_apple2.py +3 -6
  62. dodal/devices/i11/cyberstar_blower.py +34 -0
  63. dodal/devices/i11/diff_stages.py +55 -0
  64. dodal/devices/i11/mythen.py +165 -0
  65. dodal/devices/i11/nx100robot.py +153 -0
  66. dodal/devices/i11/spinner.py +30 -0
  67. dodal/devices/i13_1/merlin_controller.py +4 -4
  68. dodal/devices/i19/diffractometer.py +34 -0
  69. dodal/devices/i19/shutter.py +11 -1
  70. dodal/devices/i22/dcm.py +1 -1
  71. dodal/devices/i22/fswitch.py +3 -12
  72. dodal/devices/i24/aperture.py +3 -3
  73. dodal/devices/i24/dcm.py +11 -15
  74. dodal/devices/i24/dual_backlight.py +11 -12
  75. dodal/devices/i24/pmac.py +8 -7
  76. dodal/devices/mx_phase1/beamstop.py +10 -11
  77. dodal/devices/oav/pin_image_recognition/__init__.py +0 -3
  78. dodal/devices/p60/enums.py +8 -8
  79. dodal/devices/p60/lab_xray_source.py +3 -2
  80. dodal/devices/pressure_jump_cell.py +77 -123
  81. dodal/devices/scintillator.py +76 -4
  82. dodal/devices/smargon.py +2 -2
  83. dodal/devices/synchrotron.py +1 -2
  84. dodal/devices/thawer.py +6 -11
  85. dodal/devices/undulator.py +3 -8
  86. dodal/devices/util/epics_util.py +1 -1
  87. dodal/devices/watsonmarlow323_pump.py +7 -7
  88. dodal/devices/xbpm_feedback.py +4 -6
  89. dodal/devices/xspress3/xspress3.py +0 -5
  90. dodal/devices/zocalo/zocalo_results.py +1 -3
  91. dodal/testing/__init__.py +0 -0
  92. dodal/testing/electron_analyser/__init__.py +6 -0
  93. dodal/testing/electron_analyser/device_factory.py +59 -0
  94. dodal/devices/CTAB.py +0 -41
  95. {dls_dodal-1.55.1.dist-info → dls_dodal-1.56.0.dist-info}/WHEEL +0 -0
  96. {dls_dodal-1.55.1.dist-info → dls_dodal-1.56.0.dist-info}/entry_points.txt +0 -0
  97. {dls_dodal-1.55.1.dist-info → dls_dodal-1.56.0.dist-info}/licenses/LICENSE +0 -0
  98. {dls_dodal-1.55.1.dist-info → dls_dodal-1.56.0.dist-info}/top_level.txt +0 -0
dodal/beamlines/i24.py CHANGED
@@ -100,7 +100,8 @@ def dcm() -> DCM:
100
100
  If this is called when already instantiated in i24, it will return the existing object.
101
101
  """
102
102
  return DCM(
103
- prefix=PREFIX.beamline_prefix,
103
+ prefix=f"{PREFIX.beamline_prefix}-DI-DCM-01",
104
+ motion_prefix=f"{PREFIX.beamline_prefix}-MO-DCM-01",
104
105
  )
105
106
 
106
107
 
@@ -136,10 +137,7 @@ def pmac() -> PMAC:
136
137
  """Get the i24 PMAC device, instantiate it if it hasn't already been.
137
138
  If this is called when already instantiated in i24, it will return the existing object.
138
139
  """
139
- # prefix not BL but ME14E
140
- return PMAC(
141
- "ME14E-MO-CHIP-01:",
142
- )
140
+ return PMAC(PREFIX.beamline_prefix)
143
141
 
144
142
 
145
143
  @device_factory()
@@ -155,9 +153,7 @@ def vgonio() -> VerticalGoniometer:
155
153
  """Get the i24 vertical goniometer device, instantiate it if it hasn't already been.
156
154
  If this is called when already instantiated, it will return the existing object.
157
155
  """
158
- return VerticalGoniometer(
159
- f"{PREFIX.beamline_prefix}-MO-VGON-01:",
160
- )
156
+ return VerticalGoniometer(f"{PREFIX.beamline_prefix}-MO-VGON-01:")
161
157
 
162
158
 
163
159
  @device_factory()
@@ -176,17 +172,13 @@ def shutter() -> HutchShutter:
176
172
  """Get the i24 hutch shutter device, instantiate it if it hasn't already been.
177
173
  If this is called when already instantiated, it will return the existing object.
178
174
  """
179
- return HutchShutter(
180
- f"{PREFIX.beamline_prefix}-PS-SHTR-01:",
181
- )
175
+ return HutchShutter(f"{PREFIX.beamline_prefix}-PS-SHTR-01:")
182
176
 
183
177
 
184
178
  @device_factory()
185
179
  def focus_mirrors() -> FocusMirrorsMode:
186
180
  """Get the i24 focus mirror devise to find the beam size."""
187
- return FocusMirrorsMode(
188
- f"{PREFIX.beamline_prefix}-OP-MFM-01:",
189
- )
181
+ return FocusMirrorsMode(f"{PREFIX.beamline_prefix}-OP-MFM-01:")
190
182
 
191
183
 
192
184
  @device_factory()
dodal/beamlines/p38.py CHANGED
@@ -145,6 +145,7 @@ def hfm() -> FocusingMirror:
145
145
  @device_factory(mock=True)
146
146
  def dcm() -> DCM:
147
147
  return DCM(
148
+ prefix=f"{PREFIX.beamline_prefix}-MO-DCM-01:",
148
149
  temperature_prefix=f"{PREFIX.beamline_prefix}-DI-DCM-01:",
149
150
  crystal_1_metadata=make_crystal_metadata_from_material(
150
151
  MaterialsEnum.Si, (1, 1, 1)
dodal/beamlines/p60.py CHANGED
@@ -2,6 +2,7 @@ from dodal.common.beamlines.beamline_utils import (
2
2
  device_factory,
3
3
  )
4
4
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
5
+ from dodal.devices.electron_analyser import SelectedSource
5
6
  from dodal.devices.electron_analyser.vgscienta import VGScientaAnalyserDriverIO
6
7
  from dodal.devices.p60 import (
7
8
  LabXraySource,
@@ -34,8 +35,8 @@ def mg_kalpha_source() -> LabXraySourceReadable:
34
35
  @device_factory()
35
36
  def analyser_driver() -> VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]:
36
37
  energy_sources = {
37
- "source1": al_kalpha_source().energy_ev,
38
- "source2": mg_kalpha_source().energy_ev,
38
+ SelectedSource.SOURCE1: al_kalpha_source().energy_ev,
39
+ SelectedSource.SOURCE2: mg_kalpha_source().energy_ev,
39
40
  }
40
41
  return VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy](
41
42
  prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:",
dodal/cli.py CHANGED
@@ -1,12 +1,14 @@
1
1
  import os
2
2
  from collections.abc import Mapping
3
+ from pathlib import Path
3
4
 
4
5
  import click
5
6
  from bluesky.run_engine import RunEngine
6
- from ophyd_async.core import NotConnected
7
+ from ophyd_async.core import NotConnected, StaticPathProvider, UUIDFilenameProvider
7
8
  from ophyd_async.plan_stubs import ensure_connected
8
9
 
9
10
  from dodal.beamlines import all_beamline_names, module_name_for_beamline
11
+ from dodal.common.beamlines.beamline_utils import set_path_provider
10
12
  from dodal.utils import AnyDevice, filter_ophyd_devices, make_all_devices
11
13
 
12
14
  from . import __version__
@@ -47,6 +49,10 @@ def connect(beamline: str, all: bool, sim_backend: bool) -> None:
47
49
 
48
50
  os.environ["BEAMLINE"] = beamline
49
51
 
52
+ # We need to make a fake path provider for any detectors that need one,
53
+ # it is not used in dodal connect
54
+ _spoof_path_provider()
55
+
50
56
  module_name = module_name_for_beamline(beamline)
51
57
  full_module_path = f"dodal.beamlines.{module_name}"
52
58
 
@@ -115,3 +121,7 @@ def _connect_devices(
115
121
  name: device for name, device in devices.items() if name not in exceptions
116
122
  }
117
123
  return successful_devices, exceptions
124
+
125
+
126
+ def _spoof_path_provider() -> None:
127
+ set_path_provider(StaticPathProvider(UUIDFilenameProvider(), Path("/tmp")))
dodal/common/__init__.py CHANGED
@@ -1,10 +1,14 @@
1
1
  from .coordination import group_uuid, inject
2
+ from .enums import EnabledDisabledUpper, InOutUpper, OnOffUpper
2
3
  from .maths import in_micros, step_to_num
3
4
  from .types import MsgGenerator, PlanGenerator
4
5
 
5
6
  __all__ = [
6
7
  "group_uuid",
7
8
  "inject",
9
+ "EnabledDisabledUpper",
10
+ "InOutUpper",
11
+ "OnOffUpper",
8
12
  "in_micros",
9
13
  "MsgGenerator",
10
14
  "PlanGenerator",
@@ -8,7 +8,7 @@ BEAMLINE_PARAMETER_KEYWORDS = ["FB", "FULL", "deadtime"]
8
8
 
9
9
  BEAMLINE_PARAMETER_PATHS = {
10
10
  "i03": "/dls_sw/i03/software/daq_configuration/domain/beamlineParameters",
11
- "i04": "/dls_sw/i04/software/gda_versions/gda_9_37/workspace_git/gda-mx.git/configurations/i04-config/scripts/beamlineParameters",
11
+ "i04": "/dls_sw/i04/software/daq_configuration/domain/beamlineParameters",
12
12
  }
13
13
 
14
14
 
@@ -169,4 +169,8 @@ def get_path_provider() -> PathProvider:
169
169
 
170
170
  def clear_path_provider() -> None:
171
171
  global PATH_PROVIDER
172
- del PATH_PROVIDER
172
+ try:
173
+ del PATH_PROVIDER
174
+ except NameError:
175
+ # In this case the path provider was never set so we can do nothing
176
+ pass
dodal/common/enums.py ADDED
@@ -0,0 +1,19 @@
1
+ from ophyd_async.core import StrictEnum
2
+
3
+ # Any capitalised enums needs to be removed and replaced with ones from ophyd-async.core
4
+ # https://github.com/DiamondLightSource/dodal/issues/1416
5
+
6
+
7
+ class OnOffUpper(StrictEnum):
8
+ ON = "ON"
9
+ OFF = "OFF"
10
+
11
+
12
+ class EnabledDisabledUpper(StrictEnum):
13
+ ENABLED = "ENABLED"
14
+ DISABLED = "DISABLED"
15
+
16
+
17
+ class InOutUpper(StrictEnum):
18
+ IN = "IN"
19
+ OUT = "OUT"
@@ -0,0 +1,83 @@
1
+ from ophyd_async.core import WatchableAsyncStatus, Watcher
2
+
3
+ from dodal.log import LOGGER
4
+
5
+ Number = int | float
6
+
7
+
8
+ class _LogOnPercentageProgressWatcher(Watcher[Number]):
9
+ def __init__(
10
+ self,
11
+ status: WatchableAsyncStatus[Number],
12
+ message_prefix: str,
13
+ percent_interval: Number = 25,
14
+ ):
15
+ status.watch(self)
16
+ self.percent_interval = percent_interval
17
+ self._current_percent_interval = 0
18
+ self.message_prefix = message_prefix
19
+ if self.percent_interval <= 0:
20
+ raise ValueError(
21
+ f"Percent interval on class _LogOnPercentageProgressWatcher must be a positive number, but received {self.percent_interval}"
22
+ )
23
+
24
+ def __call__(
25
+ self,
26
+ current: Number | None = None,
27
+ initial: Number | None = None,
28
+ target: Number | None = None,
29
+ name: str | None = None,
30
+ unit: str | None = None,
31
+ precision: int | None = None,
32
+ fraction: float | None = None,
33
+ time_elapsed: float | None = None,
34
+ time_remaining: float | None = None,
35
+ ):
36
+ if (
37
+ isinstance(current, Number)
38
+ and isinstance(target, Number)
39
+ and isinstance(initial, Number)
40
+ and target != initial
41
+ ):
42
+ current_percent = int(((current - initial) / (target - initial)) * 100)
43
+ if (
44
+ current_percent
45
+ >= (self._current_percent_interval + 1) * self.percent_interval
46
+ ):
47
+ LOGGER.info(f"{self.message_prefix}: {current_percent}%")
48
+ self._current_percent_interval = (
49
+ current_percent // self.percent_interval
50
+ )
51
+
52
+
53
+ def log_on_percentage_complete(
54
+ status: WatchableAsyncStatus[int | float],
55
+ message_prefix: str,
56
+ percent_interval: int = 25,
57
+ ):
58
+ """
59
+ Add watcher to a WatchableAsyncStatus status which will periodically log a message based on percentage completion
60
+
61
+ Args:
62
+ status: A WatchableAsyncStatus. For example, Ophyd-async produces this status from a Motor.set method
63
+
64
+ message_prefix: The string at the start of each of the produced logging messages
65
+
66
+ percent_interval: How often to produce logging message, in terms of percentage completion
67
+ of the status.
68
+
69
+ Note that when using with Bluesky plan stubs you will need to cast the status (as of
70
+ Bluesky v1.14.2), since a Bluesky status doesn't use generics - see https://github.com/bluesky/bluesky/issues/1948.
71
+
72
+ When running Bluesky plans using an interactive terminal, it is better to use the standard bluesky progress
73
+ bar instead of this function. See https://blueskyproject.io/bluesky/main/progress-bar.html#progress-bar
74
+
75
+ Example usage within a Bluesky plan:
76
+ yield from bps.kickoff(my_detector)
77
+ status = yield from bps.complete(my_detector, group="collection complete")
78
+ status = cast(WatchableAsyncStatus, status)
79
+ log_on_percentage_complete(status, "Data collection triggers received", 10)
80
+ yield from bps.wait("collection complete")
81
+
82
+ """
83
+ _LogOnPercentageProgressWatcher(status, message_prefix, percent_interval)
@@ -1,24 +1,19 @@
1
- from ophyd_async.core import StrictEnum
1
+ from ophyd_async.core import EnabledDisabled, OnOff, StrictEnum
2
2
  from ophyd_async.epics.core import epics_signal_rw
3
3
 
4
4
  from dodal.devices.robot import BartRobot
5
5
 
6
6
 
7
7
  class ForceBit(StrictEnum):
8
- ON = "On"
8
+ ON = OnOff.ON.value
9
9
  NO = "No"
10
- OFF = "Off"
11
-
12
-
13
- class LidHeatEnable(StrictEnum):
14
- ENABLED = "Enabled"
15
- DISABLED = "Disabled"
10
+ OFF = OnOff.OFF.value
16
11
 
17
12
 
18
13
  class LaserRobot(BartRobot):
19
14
  def __init__(self, prefix: str, name: str = "") -> None:
20
15
  self.dewar_lid_heater = epics_signal_rw(
21
- LidHeatEnable, prefix + "DW_1_ENABLED", prefix + "DW_1_CTRL"
16
+ EnabledDisabled, prefix + "DW_1_ENABLED", prefix + "DW_1_CTRL"
22
17
  )
23
18
  self.cryojet_retract = epics_signal_rw(ForceBit, prefix + "OP_24_FORCE_OPTION")
24
19
  self.set_beamline_safe = epics_signal_rw(
@@ -29,6 +29,7 @@ class _GDAParamApertureValue(StrictEnum):
29
29
  SMALL = "SMALL_APERTURE"
30
30
  MEDIUM = "MEDIUM_APERTURE"
31
31
  LARGE = "LARGE_APERTURE"
32
+ MANUAL_LOAD = "MANUAL_LOAD"
32
33
 
33
34
 
34
35
  class AperturePosition(BaseModel):
@@ -92,7 +93,7 @@ class AperturePosition(BaseModel):
92
93
  class ApertureValue(StrictEnum):
93
94
  """The possible apertures that can be selected.
94
95
 
95
- Changing these means changing the external paramter model of Hyperion.
96
+ Changing these means changing the external parameter model of Hyperion.
96
97
  See https://github.com/DiamondLightSource/mx-bluesky/issues/760
97
98
  """
98
99
 
@@ -100,6 +101,7 @@ class ApertureValue(StrictEnum):
100
101
  MEDIUM = "MEDIUM_APERTURE"
101
102
  LARGE = "LARGE_APERTURE"
102
103
  OUT_OF_BEAM = "Out of beam"
104
+ PARKED = "Parked" # Parked under the collimation table for manual load
103
105
 
104
106
  def __str__(self):
105
107
  return self.name.capitalize()
@@ -121,6 +123,9 @@ def load_positions_from_beamline_parameters(
121
123
  ApertureValue.LARGE: AperturePosition.from_gda_params(
122
124
  _GDAParamApertureValue.LARGE, 100, params
123
125
  ),
126
+ ApertureValue.PARKED: AperturePosition.from_gda_params(
127
+ _GDAParamApertureValue.MANUAL_LOAD, 0, params
128
+ ),
124
129
  }
125
130
 
126
131
 
@@ -158,13 +163,14 @@ class ApertureScatterguard(StandardReadable, Preparable):
158
163
 
159
164
  def __init__(
160
165
  self,
166
+ aperture_prefix: str,
167
+ scatterguard_prefix: str,
161
168
  loaded_positions: dict[ApertureValue, AperturePosition],
162
169
  tolerances: AperturePosition,
163
- prefix: str = "",
164
170
  name: str = "",
165
171
  ) -> None:
166
- self.aperture = Aperture(prefix + "-MO-MAPT-01:")
167
- self.scatterguard = XYStage(prefix + "-MO-SCAT-01:")
172
+ self.aperture = Aperture(aperture_prefix)
173
+ self.scatterguard = XYStage(scatterguard_prefix)
168
174
  self._loaded_positions = loaded_positions
169
175
  self._tolerances = tolerances
170
176
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
@@ -175,6 +181,7 @@ class ApertureScatterguard(StandardReadable, Preparable):
175
181
  medium=self.aperture.medium,
176
182
  small=self.aperture.small,
177
183
  current_ap_y=self.aperture.y.user_readback,
184
+ current_ap_z=self.aperture.z.user_readback,
178
185
  )
179
186
 
180
187
  self.radius = derived_signal_r(
@@ -196,13 +203,30 @@ class ApertureScatterguard(StandardReadable, Preparable):
196
203
 
197
204
  super().__init__(name)
198
205
 
206
+ async def _unpark(self, position_to_move_to: ApertureValue):
207
+ """When the aperture is parked it is under the collimation table. It needs to be
208
+ moved out from under the table before it is moved up to beam height.
209
+ """
210
+ position = self._loaded_positions[position_to_move_to]
211
+ await self.aperture.z.set(position.aperture_z)
212
+
199
213
  async def _set_current_aperture_position(self, value: ApertureValue) -> None:
214
+ if value == ApertureValue.PARKED:
215
+ raise NotImplementedError(
216
+ "Currently not able to park aperture/scatterguard, see https://github.com/DiamondLightSource/mx-bluesky/issues/1197"
217
+ )
218
+
200
219
  position = self._loaded_positions[value]
220
+
221
+ current_ap_y = await self.aperture.y.user_readback.get_value()
222
+ current_ap_z = await self.aperture.z.user_readback.get_value()
223
+ if self._is_in_position(ApertureValue.PARKED, current_ap_y, current_ap_z):
224
+ await self._unpark(value)
225
+
201
226
  await self._check_safe_to_move(position.aperture_z)
202
227
 
203
228
  if value == ApertureValue.OUT_OF_BEAM:
204
- out_y = self._loaded_positions[ApertureValue.OUT_OF_BEAM].aperture_y
205
- await self.aperture.y.set(out_y)
229
+ await self.aperture.y.set(position.aperture_y)
206
230
  else:
207
231
  await self._safe_move_whilst_in_beam(position)
208
232
 
@@ -240,12 +264,22 @@ class ApertureScatterguard(StandardReadable, Preparable):
240
264
  def _get_current_radius(self, current_aperture: ApertureValue) -> float:
241
265
  return self._loaded_positions[current_aperture].radius
242
266
 
243
- def _is_out_of_beam(self, current_ap_y: float) -> bool:
244
- out_ap_y = self._loaded_positions[ApertureValue.OUT_OF_BEAM].aperture_y
245
- return current_ap_y <= out_ap_y + self._tolerances.aperture_y
267
+ def _is_in_position(
268
+ self, position: ApertureValue, current_ap_y: float, current_ap_z: float
269
+ ) -> bool:
270
+ position_y = self._loaded_positions[position].aperture_y
271
+ position_z = self._loaded_positions[position].aperture_z
272
+ y_matches = abs(current_ap_y - position_y) <= self._tolerances.aperture_y
273
+ z_matches = abs(current_ap_z - position_z) <= self._tolerances.aperture_z
274
+ return y_matches and z_matches
246
275
 
247
276
  def _get_current_aperture_position(
248
- self, large: float, medium: float, small: float, current_ap_y: float
277
+ self,
278
+ large: float,
279
+ medium: float,
280
+ small: float,
281
+ current_ap_y: float,
282
+ current_ap_z: float,
249
283
  ) -> ApertureValue:
250
284
  if large == 1:
251
285
  return ApertureValue.LARGE
@@ -253,7 +287,11 @@ class ApertureScatterguard(StandardReadable, Preparable):
253
287
  return ApertureValue.MEDIUM
254
288
  elif small == 1:
255
289
  return ApertureValue.SMALL
256
- elif self._is_out_of_beam(current_ap_y):
290
+ elif self._is_in_position(ApertureValue.PARKED, current_ap_y, current_ap_z):
291
+ return ApertureValue.PARKED
292
+ elif self._is_in_position(
293
+ ApertureValue.OUT_OF_BEAM, current_ap_y, current_ap_z
294
+ ):
257
295
  return ApertureValue.OUT_OF_BEAM
258
296
 
259
297
  raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
@@ -317,7 +355,9 @@ class ApertureScatterguard(StandardReadable, Preparable):
317
355
  Moving the assembly whilst out of the beam has no collision risk so we can just
318
356
  move all the motors together.
319
357
  """
320
- if self._is_out_of_beam(await self.aperture.y.user_readback.get_value()):
358
+ current_y = await self.aperture.y.user_readback.get_value()
359
+ current_z = await self.aperture.z.user_readback.get_value()
360
+ if self._is_in_position(ApertureValue.OUT_OF_BEAM, current_y, current_z):
321
361
  aperture_x, _, aperture_z, scatterguard_x, scatterguard_y = (
322
362
  self._loaded_positions[value].values
323
363
  )
@@ -406,7 +406,6 @@ class Apple2(abc.ABC, StandardReadable, Movable):
406
406
  self,
407
407
  id_gap: UndulatorGap,
408
408
  id_phase: UndulatorPhaseAxes,
409
- prefix: str = "",
410
409
  name: str = "",
411
410
  ) -> None:
412
411
  """
@@ -1,5 +1,4 @@
1
1
  from ophyd_async.epics.adcore import (
2
- ADBaseController,
3
2
  ADBaseIO,
4
3
  ADTIFFWriter,
5
4
  AreaDetector,
@@ -7,15 +6,7 @@ from ophyd_async.epics.adcore import (
7
6
 
8
7
  from dodal.common.beamlines.beamline_utils import get_path_provider
9
8
  from dodal.common.beamlines.device_helpers import CAM_SUFFIX, TIFF_SUFFIX
10
-
11
-
12
- class ConstantDeadTimeController(ADBaseController):
13
- def __init__(self, driver: ADBaseIO, deadtime: float):
14
- super().__init__(driver)
15
- self.deadtime = deadtime
16
-
17
- def get_deadtime(self, exposure: float | None) -> float:
18
- return self.deadtime
9
+ from dodal.devices.controllers import ConstantDeadTimeController
19
10
 
20
11
 
21
12
  def software_triggered_tiff_area_detector(prefix: str, deadtime: float = 0.0):
@@ -1,35 +1,23 @@
1
1
  from asyncio import sleep
2
2
 
3
3
  from bluesky.protocols import Movable
4
- from ophyd_async.core import AsyncStatus, StandardReadable, StrictEnum
4
+ from ophyd_async.core import AsyncStatus, InOut, OnOff, StandardReadable
5
5
  from ophyd_async.epics.core import epics_signal_rw
6
6
 
7
7
 
8
- class BacklightPower(StrictEnum):
9
- ON = "On"
10
- OFF = "Off"
11
-
12
-
13
- class BacklightPosition(StrictEnum):
14
- IN = "In"
15
- OUT = "Out"
16
-
17
-
18
- class Backlight(StandardReadable, Movable[BacklightPosition]):
8
+ class Backlight(StandardReadable, Movable[InOut]):
19
9
  """Simple device to trigger the pneumatic in/out."""
20
10
 
21
11
  TIME_TO_MOVE_S = 1.0 # Tested using a stopwatch on the beamline 09/2024
22
12
 
23
13
  def __init__(self, prefix: str, name: str = "") -> None:
24
14
  with self.add_children_as_readables():
25
- self.power = epics_signal_rw(BacklightPower, prefix + "-EA-BLIT-01:TOGGLE")
26
- self.position = epics_signal_rw(
27
- BacklightPosition, prefix + "-EA-BL-01:CTRL"
28
- )
15
+ self.power = epics_signal_rw(OnOff, prefix + "-EA-BLIT-01:TOGGLE")
16
+ self.position = epics_signal_rw(InOut, prefix + "-EA-BL-01:CTRL")
29
17
  super().__init__(name)
30
18
 
31
19
  @AsyncStatus.wrap
32
- async def set(self, value: BacklightPosition):
20
+ async def set(self, value: InOut):
33
21
  """This setter will turn the backlight on when we move it in to the beam and off
34
22
  when we move it out.
35
23
 
@@ -39,9 +27,9 @@ class Backlight(StandardReadable, Movable[BacklightPosition]):
39
27
  """
40
28
  old_position = await self.position.get_value()
41
29
  await self.position.set(value)
42
- if value == BacklightPosition.OUT:
43
- await self.power.set(BacklightPower.OFF)
30
+ if value == InOut.OUT:
31
+ await self.power.set(OnOff.OFF)
44
32
  else:
45
- await self.power.set(BacklightPower.ON)
33
+ await self.power.set(OnOff.ON)
46
34
  if old_position != value:
47
35
  await sleep(self.TIME_TO_MOVE_S)
@@ -22,12 +22,9 @@ from ophyd_async.epics.core import (
22
22
  epics_signal_x,
23
23
  )
24
24
 
25
- DEFAULT_TIMEOUT = 60
26
-
25
+ from dodal.common.enums import OnOffUpper
27
26
 
28
- class BimorphMirrorOnOff(StrictEnum):
29
- ON = "ON"
30
- OFF = "OFF"
27
+ DEFAULT_TIMEOUT = 60
31
28
 
32
29
 
33
30
  class BimorphMirrorMode(StrictEnum):
@@ -54,7 +51,7 @@ class BimorphMirrorChannel(StandardReadable, EpicsDevice):
54
51
 
55
52
  target_voltage: A[SignalRW[float], PvSuffix.rbv("VTRGT"), Format.CONFIG_SIGNAL]
56
53
  output_voltage: A[SignalRW[float], PvSuffix.rbv("VOUT"), Format.HINTED_SIGNAL]
57
- status: A[SignalR[BimorphMirrorOnOff], PvSuffix("STATUS"), Format.CONFIG_SIGNAL]
54
+ status: A[SignalR[OnOffUpper], PvSuffix("STATUS"), Format.CONFIG_SIGNAL]
58
55
  shift: A[SignalW[float], PvSuffix("SHIFT")]
59
56
 
60
57
 
@@ -87,7 +84,7 @@ class BimorphMirror(StandardReadable, Movable[list[float]]):
87
84
  for i in range(1, number_of_channels + 1)
88
85
  }
89
86
  )
90
- self.enabled = epics_signal_w(BimorphMirrorOnOff, f"{prefix}ONOFF")
87
+ self.enabled = epics_signal_w(OnOffUpper, f"{prefix}ONOFF")
91
88
  self.commit_target_voltages = epics_signal_x(f"{prefix}ALLTRGT.PROC")
92
89
  self.status = epics_signal_r(BimorphMirrorStatus, f"{prefix}STATUS")
93
90
  self.err = epics_signal_r(str, f"{prefix}ERR")
@@ -0,0 +1,36 @@
1
+ from ophyd_async.core import StandardReadable
2
+ from ophyd_async.epics.motor import Motor
3
+
4
+
5
+ class CollimationTable(StandardReadable):
6
+ """Basic collimation table device for motion plus the motion disable signal
7
+ when laser curtain triggered and hutch not locked.
8
+
9
+ The table has 3 physical vertical motors, the jacks. 1 upstream and 2 downstream.
10
+ The two downstream jacks are labelled as outboard (away from the ring) and
11
+ inboard (towards the ring).
12
+ Together these 3 jacks provide compound motion for vertical motion and pitch/roll.
13
+ There are 2 physical horizontal motors 1 upstream, 1 downstream. These provide yaw.
14
+
15
+ Table motion is disabled by an object being within the laser curtain area and can be
16
+ overridden by use of the dead man's handle device or locking the hutch. The effect of
17
+ these disabling systems is to cut power to the motors - signal for this is crate_power
18
+ """
19
+
20
+ def __init__(self, prefix: str, name: str = ""):
21
+ with self.add_children_as_readables():
22
+ self.inboard_y = Motor(f"{prefix}:INBOARDY")
23
+ self.outboard_y = Motor(f"{prefix}:OUTBOARDY")
24
+ self.upstream_y = Motor(f"{prefix}:UPSTREAMY")
25
+ self.combined_downstream_y = Motor(f"{prefix}:DOWNSTREAMY")
26
+ self.combined_all_y = Motor(f"{prefix}:Y")
27
+
28
+ self.downstream_x = Motor(f"{prefix}:DOWNSTREAMX")
29
+ self.upstream_x = Motor(f"{prefix}:UPSTREAMX")
30
+ self.combined_all_x = Motor(f"{prefix}:X")
31
+
32
+ self.pitch = Motor(f"{prefix}:PITCH")
33
+ self.roll = Motor(f"{prefix}:ROLL")
34
+ self.yaw = Motor(f"{prefix}:YAW")
35
+
36
+ super().__init__(name)
@@ -0,0 +1,21 @@
1
+ from typing import TypeVar
2
+
3
+ from ophyd_async.epics.adcore import (
4
+ ADBaseController,
5
+ ADBaseIO,
6
+ )
7
+
8
+ ADBaseIOT = TypeVar("ADBaseIOT", bound=ADBaseIO)
9
+
10
+
11
+ class ConstantDeadTimeController(ADBaseController[ADBaseIOT]):
12
+ """
13
+ ADBaseController with a configured constant deadtime for a driver of type ADBaseIO.
14
+ """
15
+
16
+ def __init__(self, driver: ADBaseIOT, deadtime: float):
17
+ super().__init__(driver)
18
+ self.deadtime = deadtime
19
+
20
+ def get_deadtime(self, exposure: float | None) -> float:
21
+ return self.deadtime