dls-dodal 1.49.0__py3-none-any.whl → 1.51.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 (72) hide show
  1. {dls_dodal-1.49.0.dist-info → dls_dodal-1.51.0.dist-info}/METADATA +3 -4
  2. {dls_dodal-1.49.0.dist-info → dls_dodal-1.51.0.dist-info}/RECORD +65 -60
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/adsim.py +5 -3
  5. dodal/beamlines/b01_1.py +41 -5
  6. dodal/beamlines/b07.py +11 -1
  7. dodal/beamlines/b07_1.py +11 -1
  8. dodal/beamlines/b16.py +8 -4
  9. dodal/beamlines/b21.py +148 -0
  10. dodal/beamlines/i03.py +6 -11
  11. dodal/beamlines/i04.py +5 -5
  12. dodal/beamlines/i09.py +22 -1
  13. dodal/beamlines/i09_1.py +9 -1
  14. dodal/beamlines/i09_2.py +24 -0
  15. dodal/beamlines/i10.py +5 -6
  16. dodal/beamlines/i13_1.py +5 -5
  17. dodal/beamlines/i18.py +5 -6
  18. dodal/beamlines/i22.py +7 -1
  19. dodal/beamlines/i24.py +3 -3
  20. dodal/beamlines/p45.py +4 -3
  21. dodal/beamlines/p60.py +18 -1
  22. dodal/beamlines/p99.py +5 -5
  23. dodal/beamlines/training_rig.py +3 -3
  24. dodal/common/beamlines/beamline_utils.py +5 -2
  25. dodal/devices/aithre_lasershaping/goniometer.py +4 -5
  26. dodal/devices/aperture.py +4 -7
  27. dodal/devices/aperturescatterguard.py +2 -2
  28. dodal/devices/b07/__init__.py +3 -0
  29. dodal/devices/b07/grating.py +9 -0
  30. dodal/devices/b07_1/__init__.py +3 -0
  31. dodal/devices/b07_1/grating.py +10 -0
  32. dodal/devices/detector/detector_motion.py +19 -17
  33. dodal/devices/electron_analyser/abstract/base_driver_io.py +24 -25
  34. dodal/devices/electron_analyser/detector.py +3 -13
  35. dodal/devices/electron_analyser/specs/detector.py +9 -3
  36. dodal/devices/electron_analyser/specs/driver_io.py +5 -2
  37. dodal/devices/electron_analyser/vgscienta/detector.py +9 -3
  38. dodal/devices/electron_analyser/vgscienta/driver_io.py +5 -6
  39. dodal/devices/electron_analyser/vgscienta/region.py +0 -1
  40. dodal/devices/fast_grid_scan.py +1 -2
  41. dodal/devices/hutch_shutter.py +6 -6
  42. dodal/devices/i04/constants.py +1 -1
  43. dodal/devices/i09/__init__.py +4 -0
  44. dodal/devices/i09/dcm.py +26 -0
  45. dodal/devices/i09/grating.py +7 -0
  46. dodal/devices/i10/mirrors.py +4 -6
  47. dodal/devices/i10/rasor/rasor_motors.py +0 -14
  48. dodal/devices/i19/beamstop.py +3 -7
  49. dodal/devices/i24/aperture.py +4 -6
  50. dodal/devices/i24/beamstop.py +5 -8
  51. dodal/devices/i24/pmac.py +4 -8
  52. dodal/devices/motors.py +92 -35
  53. dodal/devices/p45.py +0 -12
  54. dodal/devices/p60/__init__.py +3 -0
  55. dodal/devices/p60/lab_xray_source.py +21 -0
  56. dodal/devices/pgm.py +1 -1
  57. dodal/devices/robot.py +11 -7
  58. dodal/devices/smargon.py +8 -9
  59. dodal/devices/zocalo/zocalo_results.py +27 -78
  60. dodal/plans/bimorph.py +333 -0
  61. dodal/plans/configure_arm_trigger_and_disarm_detector.py +7 -5
  62. dodal/devices/adsim.py +0 -13
  63. dodal/devices/i18/table.py +0 -14
  64. dodal/devices/i18/thor_labs_stage.py +0 -12
  65. dodal/devices/i24/i24_detector_motion.py +0 -12
  66. dodal/devices/scatterguard.py +0 -11
  67. dodal/devices/training_rig/__init__.py +0 -0
  68. dodal/devices/training_rig/sample_stage.py +0 -10
  69. {dls_dodal-1.49.0.dist-info → dls_dodal-1.51.0.dist-info}/WHEEL +0 -0
  70. {dls_dodal-1.49.0.dist-info → dls_dodal-1.51.0.dist-info}/entry_points.txt +0 -0
  71. {dls_dodal-1.49.0.dist-info → dls_dodal-1.51.0.dist-info}/licenses/LICENSE +0 -0
  72. {dls_dodal-1.49.0.dist-info → dls_dodal-1.51.0.dist-info}/top_level.txt +0 -0
@@ -1,9 +1,10 @@
1
1
  import asyncio
2
2
  from abc import ABC, abstractmethod
3
+ from collections.abc import Mapping
3
4
  from typing import Generic, TypeVar
4
5
 
5
6
  import numpy as np
6
- from bluesky.protocols import Movable, Preparable
7
+ from bluesky.protocols import Movable
7
8
  from ophyd_async.core import (
8
9
  Array1D,
9
10
  AsyncStatus,
@@ -16,7 +17,6 @@ from ophyd_async.core import (
16
17
  )
17
18
  from ophyd_async.epics.adcore import ADBaseIO
18
19
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
19
- from ophyd_async.epics.motor import Motor
20
20
 
21
21
  from dodal.devices.electron_analyser.abstract.base_region import (
22
22
  TAbstractBaseRegion,
@@ -29,7 +29,6 @@ class AbstractAnalyserDriverIO(
29
29
  ABC,
30
30
  StandardReadable,
31
31
  ADBaseIO,
32
- Preparable,
33
32
  Movable[TAbstractBaseRegion],
34
33
  Generic[TAbstractBaseRegion],
35
34
  ):
@@ -39,8 +38,14 @@ class AbstractAnalyserDriverIO(
39
38
  """
40
39
 
41
40
  def __init__(
42
- self, prefix: str, acquisition_mode_type: type[StrictEnum], name: str = ""
41
+ self,
42
+ prefix: str,
43
+ acquisition_mode_type: type[StrictEnum],
44
+ energy_sources: Mapping[str, SignalR[float]],
45
+ name: str = "",
43
46
  ) -> None:
47
+ self.energy_sources = energy_sources
48
+
44
49
  with self.add_children_as_readables():
45
50
  self.image = epics_signal_r(Array1D[np.float64], prefix + "IMAGE")
46
51
  self.spectrum = epics_signal_r(Array1D[np.float64], prefix + "INT_SPECTRUM")
@@ -92,26 +97,6 @@ class AbstractAnalyserDriverIO(
92
97
 
93
98
  super().__init__(prefix=prefix, name=name)
94
99
 
95
- @AsyncStatus.wrap
96
- async def prepare(self, value: Motor):
97
- """
98
- Prepare the driver for a region by passing in the energy source motor selected
99
- by a region.
100
-
101
- Args:
102
- value: The motor that contains the information on the current excitation
103
- energy. Needed to prepare region for epics to accuratly calculate
104
- kinetic energy for an energy scan when in binding energy mode.
105
- """
106
- energy_source = value
107
- excitation_energy_value = await energy_source.user_readback.get_value() # eV
108
- excitation_energy_source_name = energy_source.name
109
-
110
- await asyncio.gather(
111
- self.excitation_energy.set(excitation_energy_value),
112
- self.excitation_energy_source.set(excitation_energy_source_name),
113
- )
114
-
115
100
  @AsyncStatus.wrap
116
101
  async def set(self, region: TAbstractBaseRegion):
117
102
  """
@@ -121,10 +106,13 @@ class AbstractAnalyserDriverIO(
121
106
  Args:
122
107
  region: Contains the parameters to setup the driver for a scan.
123
108
  """
109
+
110
+ source = self._get_energy_source(region.excitation_energy_source)
111
+ excitation_energy = await source.get_value() # eV
112
+
124
113
  pass_energy_type = self.pass_energy_type
125
114
  pass_energy = pass_energy_type(region.pass_energy)
126
115
 
127
- excitation_energy = await self.excitation_energy.get_value()
128
116
  low_energy = to_kinetic_energy(
129
117
  region.low_energy, region.energy_mode, excitation_energy
130
118
  )
@@ -141,8 +129,19 @@ class AbstractAnalyserDriverIO(
141
129
  self.pass_energy.set(pass_energy),
142
130
  self.iterations.set(region.iterations),
143
131
  self.acquisition_mode.set(region.acquisition_mode),
132
+ self.excitation_energy.set(excitation_energy),
133
+ self.excitation_energy_source.set(source.name),
144
134
  )
145
135
 
136
+ def _get_energy_source(self, alias_name: str) -> SignalR[float]:
137
+ energy_source = self.energy_sources.get(alias_name)
138
+ if energy_source is None:
139
+ raise KeyError(
140
+ f"'{energy_source}' is an invalid energy source. Avaliable energy "
141
+ + f"sources are '{list(self.energy_sources.keys())}'"
142
+ )
143
+ return energy_source
144
+
146
145
  @abstractmethod
147
146
  def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
148
147
  """
@@ -1,11 +1,9 @@
1
1
  from typing import Generic, TypeVar
2
2
 
3
- from bluesky.protocols import Preparable
4
3
  from ophyd_async.core import (
5
4
  AsyncStatus,
6
5
  Reference,
7
6
  )
8
- from ophyd_async.epics.motor import Motor
9
7
 
10
8
  from dodal.common.data_util import load_json_file_to_class
11
9
  from dodal.devices.electron_analyser.abstract.base_detector import (
@@ -22,7 +20,6 @@ from dodal.devices.electron_analyser.abstract.base_region import (
22
20
 
23
21
  class ElectronAnalyserRegionDetector(
24
22
  AbstractElectronAnalyserDetector[TAbstractAnalyserDriverIO],
25
- Preparable,
26
23
  Generic[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
27
24
  ):
28
25
  """
@@ -48,16 +45,10 @@ class ElectronAnalyserRegionDetector(
48
45
  return self._driver_ref()
49
46
 
50
47
  @AsyncStatus.wrap
51
- async def prepare(self, value: Motor) -> None:
52
- """
53
- Prepare driver with the region stored and energy_source motor.
54
-
55
- Args:
56
- value: The excitation energy source that the region has selected.
57
- """
58
- excitation_energy_source = value
59
- await self.driver.prepare(excitation_energy_source)
48
+ async def trigger(self) -> None:
49
+ # Configure region parameters on the driver first before data collection.
60
50
  await self.driver.set(self.region)
51
+ super().trigger()
61
52
 
62
53
 
63
54
  TElectronAnalyserRegionDetector = TypeVar(
@@ -82,7 +73,6 @@ class ElectronAnalyserDetector(
82
73
 
83
74
  def __init__(
84
75
  self,
85
- prefix: str,
86
76
  sequence_class: type[TAbstractBaseSequence],
87
77
  driver: TAbstractAnalyserDriverIO,
88
78
  name: str = "",
@@ -1,3 +1,7 @@
1
+ from collections.abc import Mapping
2
+
3
+ from ophyd_async.core import SignalR
4
+
1
5
  from dodal.devices.electron_analyser.detector import (
2
6
  ElectronAnalyserDetector,
3
7
  )
@@ -8,6 +12,8 @@ from dodal.devices.electron_analyser.specs.region import SpecsRegion, SpecsSeque
8
12
  class SpecsDetector(
9
13
  ElectronAnalyserDetector[SpecsAnalyserDriverIO, SpecsSequence, SpecsRegion]
10
14
  ):
11
- def __init__(self, prefix: str, name: str = ""):
12
- driver = SpecsAnalyserDriverIO(prefix=prefix)
13
- super().__init__(prefix, SpecsSequence, driver, name)
15
+ def __init__(
16
+ self, prefix: str, energy_sources: Mapping[str, SignalR[float]], name: str = ""
17
+ ):
18
+ driver = SpecsAnalyserDriverIO(prefix, energy_sources)
19
+ super().__init__(SpecsSequence, driver, name)
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ from collections.abc import Mapping
2
3
 
3
4
  import numpy as np
4
5
  from ophyd_async.core import (
@@ -18,7 +19,9 @@ from dodal.devices.electron_analyser.specs.region import SpecsRegion
18
19
 
19
20
 
20
21
  class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO[SpecsRegion]):
21
- def __init__(self, prefix: str, name: str = "") -> None:
22
+ def __init__(
23
+ self, prefix: str, energy_sources: Mapping[str, SignalR[float]], name: str = ""
24
+ ) -> None:
22
25
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
23
26
  # Used for setting up region data acquisition.
24
27
  self.psu_mode = epics_signal_rw(str, prefix + "SCAN_RANGE")
@@ -29,7 +32,7 @@ class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO[SpecsRegion]):
29
32
  self.min_angle_axis = epics_signal_r(float, prefix + "Y_MIN_RBV")
30
33
  self.max_angle_axis = epics_signal_r(float, prefix + "Y_MAX_RBV")
31
34
 
32
- super().__init__(prefix, AcquisitionMode, name)
35
+ super().__init__(prefix, AcquisitionMode, energy_sources, name)
33
36
 
34
37
  @AsyncStatus.wrap
35
38
  async def set(self, region: SpecsRegion):
@@ -1,3 +1,7 @@
1
+ from collections.abc import Mapping
2
+
3
+ from ophyd_async.core import SignalR
4
+
1
5
  from dodal.devices.electron_analyser.detector import (
2
6
  ElectronAnalyserDetector,
3
7
  )
@@ -17,6 +21,8 @@ class VGScientaDetector(
17
21
  VGScientaRegion,
18
22
  ]
19
23
  ):
20
- def __init__(self, prefix: str, name: str = ""):
21
- driver = VGScientaAnalyserDriverIO(prefix)
22
- super().__init__(prefix, VGScientaSequence, driver, name)
24
+ def __init__(
25
+ self, prefix: str, energy_sources: Mapping[str, SignalR], name: str = ""
26
+ ):
27
+ driver = VGScientaAnalyserDriverIO(prefix, energy_sources)
28
+ super().__init__(VGScientaSequence, driver, name)
@@ -1,4 +1,5 @@
1
1
  import asyncio
2
+ from collections.abc import Mapping
2
3
 
3
4
  import numpy as np
4
5
  from ophyd_async.core import (
@@ -22,7 +23,9 @@ from dodal.devices.electron_analyser.vgscienta.region import (
22
23
 
23
24
 
24
25
  class VGScientaAnalyserDriverIO(AbstractAnalyserDriverIO[VGScientaRegion]):
25
- def __init__(self, prefix: str, name: str = "") -> None:
26
+ def __init__(
27
+ self, prefix: str, energy_sources: Mapping[str, SignalR[float]], name: str = ""
28
+ ) -> None:
26
29
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
27
30
  # Used for setting up region data acquisition.
28
31
  self.centre_energy = epics_signal_rw(float, prefix + "CENTRE_ENERGY")
@@ -32,11 +35,7 @@ class VGScientaAnalyserDriverIO(AbstractAnalyserDriverIO[VGScientaRegion]):
32
35
  self.y_channel_size = epics_signal_rw(int, prefix + "SizeY")
33
36
  self.detector_mode = epics_signal_rw(DetectorMode, prefix + "DETECTOR_MODE")
34
37
 
35
- with self.add_children_as_readables():
36
- # Used to read detector data after acqusition.
37
- self.external_io = epics_signal_r(Array1D[np.float64], prefix + "EXTIO")
38
-
39
- super().__init__(prefix, AcquisitionMode, name)
38
+ super().__init__(prefix, AcquisitionMode, energy_sources, name)
40
39
 
41
40
  @AsyncStatus.wrap
42
41
  async def set(self, region: VGScientaRegion):
@@ -28,7 +28,6 @@ class VGScientaRegion(AbstractBaseRegion[AcquisitionMode]):
28
28
  fix_energy: float = 9.0
29
29
  total_steps: float = 13.0
30
30
  total_time: float = 13.0
31
- exposure_time: float = 1.0
32
31
  first_x_channel: int = 1
33
32
  last_x_channel: int = 1000
34
33
  first_y_channel: int = 101
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  from abc import ABC, abstractmethod
3
2
  from typing import Generic, TypeVar
4
3
 
@@ -258,7 +257,7 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
258
257
  async def complete(self):
259
258
  try:
260
259
  await wait_for_value(self.status, 0, self.COMPLETE_STATUS)
261
- except asyncio.TimeoutError:
260
+ except TimeoutError:
262
261
  LOGGER.error(
263
262
  "Hyperion timed out waiting for FGS motion to complete. This may have been caused by a goniometer stage getting stuck.\n\
264
263
  Forcibly stopping the FGS motion program..."
@@ -81,14 +81,14 @@ class HutchShutter(StandardReadable, Movable[ShutterDemand]):
81
81
 
82
82
  @AsyncStatus.wrap
83
83
  async def set(self, value: ShutterDemand):
84
- interlock_state = await self.interlock.shutter_safe_to_operate()
85
- if not interlock_state and not TEST_MODE:
86
- # If not in test mode, fail. If in test mode, the optics hutch may be open.
87
- raise ShutterNotSafeToOperateError(
88
- "The hutch has not been locked, not operating shutter."
89
- )
90
84
  if not TEST_MODE:
91
85
  if value == ShutterDemand.OPEN:
86
+ interlock_state = await self.interlock.shutter_safe_to_operate()
87
+ if not interlock_state:
88
+ # If not in test mode, fail. If in test mode, the optics hutch may be open.
89
+ raise ShutterNotSafeToOperateError(
90
+ "The hutch has not been locked, not operating shutter."
91
+ )
92
92
  await self.control.set(ShutterDemand.RESET, wait=True)
93
93
  await self.control.set(value, wait=True)
94
94
  return await wait_for_value(
@@ -4,6 +4,6 @@ from dataclasses import dataclass
4
4
 
5
5
  @dataclass(frozen=True)
6
6
  class RedisConstants:
7
- REDIS_HOST = os.environ.get("VALKEY_PROD_SVC_SERVICE_HOST", "test_redis")
7
+ REDIS_HOST = os.environ.get("VALKEY_FOR_MURKO_PROD_SVC_SERVICE_HOST", "test_redis")
8
8
  REDIS_PASSWORD = os.environ.get("VALKEY_PASSWORD", "test_redis_password")
9
9
  MURKO_REDIS_DB = 7
@@ -0,0 +1,4 @@
1
+ from dodal.devices.i09.dcm import DCM
2
+ from dodal.devices.i09.grating import I09Grating
3
+
4
+ __all__ = ["DCM", "I09Grating"]
@@ -0,0 +1,26 @@
1
+ from ophyd_async.core import derived_signal_r
2
+
3
+ from dodal.devices.common_dcm import BaseDCM, PitchAndRollCrystal, StationaryCrystal
4
+
5
+
6
+ class DCM(BaseDCM[PitchAndRollCrystal, StationaryCrystal]):
7
+ """
8
+ I09 double crystal monochromator (DCM), used to select the energy of the beam.
9
+ Differences:
10
+
11
+ 1. Can provide energy in eV via dcm.energy_in_ev read signal
12
+
13
+ This DCM is available on i09 and i09_1 endstations.
14
+ """
15
+
16
+ def __init__(self, prefix: str, name: str = "") -> None:
17
+ super().__init__(prefix, PitchAndRollCrystal, StationaryCrystal, name)
18
+ self.energy_in_ev = derived_signal_r(
19
+ self._convert_keV_to_eV, energy_signal=self.energy_in_kev.user_readback
20
+ )
21
+ # Set name so that new child signals get correct name
22
+ # need to do it until https://github.com/bluesky/ophyd-async/pull/899 merged
23
+ self.set_name(self.name)
24
+
25
+ def _convert_keV_to_eV(self, energy_signal: float) -> float:
26
+ return energy_signal * 1000
@@ -0,0 +1,7 @@
1
+ from ophyd_async.core import StrictEnum
2
+
3
+
4
+ class I09Grating(StrictEnum):
5
+ G_300 = "300 lines/mm"
6
+ G_400 = "400 lines/mm"
7
+ G_800 = "800 lines/mm"
@@ -1,18 +1,16 @@
1
- from ophyd_async.core import StandardReadable
2
1
  from ophyd_async.epics.core import epics_signal_rw
3
2
  from ophyd_async.epics.motor import Motor
4
3
 
4
+ from dodal.devices.motors import XYZStage
5
5
 
6
- class PiezoMirror(StandardReadable):
6
+
7
+ class PiezoMirror(XYZStage):
7
8
  def __init__(
8
9
  self,
9
10
  prefix: str,
10
11
  name: str = "",
11
12
  ):
12
13
  with self.add_children_as_readables():
13
- self.x = Motor(prefix + "X")
14
- self.y = Motor(prefix + "Y")
15
- self.z = Motor(prefix + "Z")
16
14
  self.yaw = Motor(prefix + "YAW")
17
15
  self.pitch = Motor(prefix + "PITCH")
18
16
  self.roll = Motor(prefix + "ROLL")
@@ -21,4 +19,4 @@ class PiezoMirror(StandardReadable):
21
19
  read_pv=prefix + "FPITCH:RBV:AI",
22
20
  write_pv=prefix + "FPITCH:DMD:AO",
23
21
  )
24
- super().__init__(name=name)
22
+ super().__init__(prefix, name)
@@ -2,20 +2,6 @@ from ophyd_async.core import StandardReadable
2
2
  from ophyd_async.epics.motor import Motor
3
3
 
4
4
 
5
- class PinHole(StandardReadable):
6
- "Two motors stage for rasor pinhole"
7
-
8
- def __init__(
9
- self,
10
- prefix: str,
11
- name: str = "",
12
- ):
13
- with self.add_children_as_readables():
14
- self.x = Motor(prefix + "X")
15
- self.y = Motor(prefix + "Y")
16
- super().__init__(name=name)
17
-
18
-
19
5
  class Diffractometer(StandardReadable):
20
6
  def __init__(
21
7
  self,
@@ -1,6 +1,7 @@
1
1
  from ophyd_async.core import StandardReadable, StrictEnum
2
2
  from ophyd_async.epics.core import epics_signal_rw, epics_signal_x
3
- from ophyd_async.epics.motor import Motor
3
+
4
+ from dodal.devices.motors import XYZStage
4
5
 
5
6
 
6
7
  class HomeGroup(StrictEnum):
@@ -18,13 +19,8 @@ class HomingControl(StandardReadable):
18
19
  super().__init__(name)
19
20
 
20
21
 
21
- class BeamStop(StandardReadable):
22
+ class BeamStop(XYZStage):
22
23
  def __init__(self, prefix: str, name: str = "") -> None:
23
- with self.add_children_as_readables():
24
- self.x = Motor(f"{prefix}X")
25
- self.y = Motor(f"{prefix}Y")
26
- self.z = Motor(f"{prefix}Z")
27
-
28
24
  self.homing = HomingControl(f"{prefix}HM", name)
29
25
 
30
26
  super().__init__(name)
@@ -1,6 +1,7 @@
1
- from ophyd_async.core import StandardReadable, StrictEnum
1
+ from ophyd_async.core import StrictEnum
2
2
  from ophyd_async.epics.core import epics_signal_rw
3
- from ophyd_async.epics.motor import Motor
3
+
4
+ from dodal.devices.motors import XYStage
4
5
 
5
6
 
6
7
  class AperturePositions(StrictEnum):
@@ -10,7 +11,7 @@ class AperturePositions(StrictEnum):
10
11
  MANUAL = "Manual Mounting"
11
12
 
12
13
 
13
- class Aperture(StandardReadable):
14
+ class Aperture(XYStage):
14
15
  """Device to trigger the aperture motor move on I24.
15
16
 
16
17
  The aperture positioner has 4 possible positions: In, Out, Robot and Manual.
@@ -20,8 +21,5 @@ class Aperture(StandardReadable):
20
21
  """
21
22
 
22
23
  def __init__(self, prefix: str, name: str = "") -> None:
23
- self.x = Motor(prefix + "X")
24
- self.y = Motor(prefix + "Y")
25
-
26
24
  self.position = epics_signal_rw(AperturePositions, prefix + "MP:SELECT")
27
25
  super().__init__(name)
@@ -1,7 +1,9 @@
1
- from ophyd_async.core import StandardReadable, StrictEnum
1
+ from ophyd_async.core import StrictEnum
2
2
  from ophyd_async.epics.core import epics_signal_rw
3
3
  from ophyd_async.epics.motor import Motor
4
4
 
5
+ from dodal.devices.motors import XYZStage
6
+
5
7
 
6
8
  class BeamstopPositions(StrictEnum):
7
9
  CHECK_BEAM = "CheckBeam"
@@ -12,7 +14,7 @@ class BeamstopPositions(StrictEnum):
12
14
  ROBOT = "Robot"
13
15
 
14
16
 
15
- class Beamstop(StandardReadable):
17
+ class Beamstop(XYZStage):
16
18
  """Device to move the beamstop.
17
19
 
18
20
  The positioner moves the x,y,z motors when a position is selected.
@@ -25,12 +27,7 @@ class Beamstop(StandardReadable):
25
27
  """
26
28
 
27
29
  def __init__(self, prefix: str, name: str = "") -> None:
28
- self.x = Motor(prefix + "X")
29
- self.y = Motor(prefix + "Y")
30
- self.z = Motor(prefix + "Z")
31
-
32
30
  self.y_rotation = Motor(prefix + "ROTY")
33
-
34
31
  self.pos_select = epics_signal_rw(BeamstopPositions, prefix + "MP:SELECT")
35
32
 
36
- super().__init__(name)
33
+ super().__init__(prefix, name)
dodal/devices/i24/pmac.py CHANGED
@@ -9,13 +9,13 @@ from ophyd_async.core import (
9
9
  Reference,
10
10
  SignalR,
11
11
  SignalRW,
12
- StandardReadable,
13
12
  observe_signals_value,
14
13
  soft_signal_rw,
15
14
  wait_for_value,
16
15
  )
17
16
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
18
- from ophyd_async.epics.motor import Motor
17
+
18
+ from dodal.devices.motors import XYZStage
19
19
 
20
20
  HOME_STR = r"\#1hmz\#2hmz\#3hmz" # Command to home the PMAC motors
21
21
  ZERO_STR = "!x0y0z0" # Command to blend any ongoing move into new position
@@ -192,7 +192,7 @@ class ProgramAbort(Triggerable):
192
192
  )
193
193
 
194
194
 
195
- class PMAC(StandardReadable):
195
+ class PMAC(XYZStage):
196
196
  """Device to control the chip stage on I24."""
197
197
 
198
198
  def __init__(self, prefix: str, name: str = "") -> None:
@@ -209,10 +209,6 @@ class PMAC(StandardReadable):
209
209
  self.pmac_string,
210
210
  )
211
211
 
212
- self.x = Motor(prefix + "X")
213
- self.y = Motor(prefix + "Y")
214
- self.z = Motor(prefix + "Z")
215
-
216
212
  # These next signals are readback values on PVARS which are set by the motion
217
213
  # program.
218
214
  self.scanstatus = epics_signal_r(float, "BL24I-MO-STEP-14:signal:P2401")
@@ -232,4 +228,4 @@ class PMAC(StandardReadable):
232
228
  )
233
229
  self.abort_program = ProgramAbort(self.pmac_string, self.scanstatus)
234
230
 
235
- super().__init__(name)
231
+ super().__init__(prefix, name)
dodal/devices/motors.py CHANGED
@@ -1,61 +1,118 @@
1
+ from abc import ABC
2
+
1
3
  from ophyd_async.core import StandardReadable
2
4
  from ophyd_async.epics.motor import Motor
3
5
 
6
+ _X, _Y, _Z = "X", "Y", "Z"
4
7
 
5
- class XYZPositioner(StandardReadable):
6
- """
7
8
 
8
- Standard ophyd_async xyz motor stage, by combining 3 Motors,
9
- with added infix for extra flexibliy to allow different axes other than x,y,z.
9
+ class Stage(StandardReadable, ABC):
10
+ """
11
+ For these devices, the following co-ordinates are typical but not enforced:
12
+ - z is horizontal & parallel to the direction of beam travel
13
+ - y is vertical and antiparallel to the force of gravity
14
+ - x is the cross product of y🞬z
10
15
 
11
16
  Parameters
12
17
  ----------
13
18
  prefix:
14
- EPICS PV (Common part up to and including :).
19
+ Common part of the EPICS PV for all motors, including ":".
15
20
  name:
16
- name for the stage.
17
- infix:
18
- EPICS PV, default is the ("X", "Y", "Z").
19
- Notes
20
- -----
21
- Example usage::
22
- async with init_devices():
23
- xyz_stage = XYZPositioner("BLXX-MO-STAGE-XX:")
24
- Or::
25
- with init_devices():
26
- xyz_stage = XYZPositioner("BLXX-MO-STAGE-XX:", infix = ("A", "B", "C"))
27
-
21
+ Name of the stage, each child motor will be named "{name}-{field_name}"
22
+ *_infix:
23
+ Infix between the common prefix and the EPICS motor record fields for the field.
28
24
  """
29
25
 
26
+ ...
27
+
28
+
29
+ class XThetaStage(Stage):
30
+ def __init__(
31
+ self, prefix: str, name: str = "", x_infix: str = _X, theta_infix: str = "A"
32
+ ):
33
+ with self.add_children_as_readables():
34
+ self.x = Motor(prefix + x_infix)
35
+ self.theta = Motor(prefix + theta_infix)
36
+ super().__init__(name=name)
37
+
38
+
39
+ class XYStage(Stage):
40
+ def __init__(
41
+ self, prefix: str, name: str = "", x_infix: str = _X, y_infix: str = _Y
42
+ ):
43
+ with self.add_children_as_readables():
44
+ self.x = Motor(prefix + x_infix)
45
+ self.y = Motor(prefix + y_infix)
46
+ super().__init__(name=name)
47
+
48
+
49
+ class XYZStage(XYStage):
30
50
  def __init__(
31
51
  self,
32
52
  prefix: str,
33
53
  name: str = "",
34
- infix: tuple[str, str, str] = ("X", "Y", "Z"),
54
+ x_infix: str = _X,
55
+ y_infix: str = _Y,
56
+ z_infix: str = _Z,
35
57
  ):
36
58
  with self.add_children_as_readables():
37
- self.x = Motor(prefix + infix[0])
38
- self.y = Motor(prefix + infix[1])
39
- self.z = Motor(prefix + infix[2])
40
- super().__init__(name=name)
59
+ self.z = Motor(prefix + z_infix)
60
+ super().__init__(prefix, name, x_infix, y_infix)
41
61
 
42
62
 
43
- class SixAxisGonio(XYZPositioner):
63
+ class XYZThetaStage(XYZStage):
44
64
  def __init__(
45
65
  self,
46
66
  prefix: str,
47
67
  name: str = "",
48
- infix: tuple[str, str, str, str, str, str] = (
49
- "X",
50
- "Y",
51
- "Z",
52
- "KAPPA",
53
- "PHI",
54
- "OMEGA",
55
- ),
68
+ x_infix: str = _X,
69
+ y_infix: str = _Y,
70
+ z_infix: str = _Z,
71
+ theta_infix: str = _Z,
72
+ ) -> None:
73
+ with self.add_children_as_readables():
74
+ self.theta = Motor(prefix + theta_infix)
75
+ super().__init__(prefix, name, x_infix, y_infix, z_infix)
76
+
77
+
78
+ class XYPitchStage(XYStage):
79
+ def __init__(
80
+ self,
81
+ prefix: str,
82
+ x_infix: str = _X,
83
+ y_infix: str = _Y,
84
+ pitch_infix: str = "PITCH",
85
+ name: str = "",
86
+ ) -> None:
87
+ with self.add_children_as_readables():
88
+ self.pitch = Motor(prefix + pitch_infix)
89
+ super().__init__(prefix, name, x_infix, y_infix)
90
+
91
+
92
+ class SixAxisGonio(XYZStage):
93
+ def __init__(
94
+ self,
95
+ prefix: str,
96
+ name: str = "",
97
+ x_infix: str = _X,
98
+ y_infix: str = _Y,
99
+ z_infix: str = _Z,
100
+ kappa_infix: str = "KAPPA",
101
+ phi_infix: str = "PHI",
102
+ omega_infix: str = "OMEGA",
56
103
  ):
57
104
  with self.add_children_as_readables():
58
- self.kappa = Motor(prefix + infix[3])
59
- self.phi = Motor(prefix + infix[4])
60
- self.omega = Motor(prefix + infix[5])
61
- super().__init__(name=name, prefix=prefix, infix=infix[0:3])
105
+ self.kappa = Motor(prefix + kappa_infix)
106
+ self.phi = Motor(prefix + phi_infix)
107
+ self.omega = Motor(prefix + omega_infix)
108
+ super().__init__(prefix, name, x_infix, y_infix, z_infix)
109
+
110
+
111
+ class YZStage(Stage):
112
+ def __init__(
113
+ self, prefix: str, name: str = "", y_infix: str = _Y, z_infix: str = _Z
114
+ ) -> None:
115
+ with self.add_children_as_readables():
116
+ self.y = Motor(prefix + y_infix)
117
+ self.z = Motor(prefix + z_infix)
118
+ super().__init__(name)