dls-dodal 1.53.0__py3-none-any.whl → 1.55.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 (74) hide show
  1. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/METADATA +4 -6
  2. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/RECORD +74 -66
  3. dodal/_version.py +16 -3
  4. dodal/beamline_specific_utils/i05_shared.py +11 -0
  5. dodal/beamlines/__init__.py +1 -0
  6. dodal/beamlines/aithre.py +2 -3
  7. dodal/beamlines/b01_1.py +2 -2
  8. dodal/beamlines/b07.py +6 -3
  9. dodal/beamlines/b07_1.py +17 -6
  10. dodal/beamlines/b16.py +0 -1
  11. dodal/beamlines/b21.py +4 -6
  12. dodal/beamlines/i03.py +10 -31
  13. dodal/beamlines/i04.py +19 -45
  14. dodal/beamlines/i05.py +22 -0
  15. dodal/beamlines/i05_1.py +22 -0
  16. dodal/beamlines/i09.py +7 -3
  17. dodal/beamlines/i09_1.py +6 -3
  18. dodal/beamlines/i13_1.py +0 -1
  19. dodal/beamlines/i19_1.py +1 -2
  20. dodal/beamlines/i19_2.py +0 -1
  21. dodal/beamlines/i19_optics.py +1 -4
  22. dodal/beamlines/i20_1.py +38 -9
  23. dodal/beamlines/i23.py +1 -2
  24. dodal/beamlines/i24.py +0 -12
  25. dodal/beamlines/p60.py +13 -3
  26. dodal/common/beamlines/beamline_parameters.py +1 -1
  27. dodal/common/beamlines/device_helpers.py +0 -33
  28. dodal/devices/aithre_lasershaping/__init__.py +0 -0
  29. dodal/devices/aithre_lasershaping/goniometer.py +3 -26
  30. dodal/devices/aithre_lasershaping/laser_robot.py +2 -2
  31. dodal/devices/b07/__init__.py +2 -2
  32. dodal/devices/b07/enums.py +15 -0
  33. dodal/devices/b07_1/__init__.py +10 -1
  34. dodal/devices/b07_1/ccmc.py +79 -0
  35. dodal/devices/b07_1/enums.py +3 -0
  36. dodal/devices/electron_analyser/abstract/base_driver_io.py +25 -48
  37. dodal/devices/electron_analyser/abstract/base_region.py +9 -11
  38. dodal/devices/electron_analyser/abstract/types.py +12 -0
  39. dodal/devices/electron_analyser/specs/detector.py +9 -9
  40. dodal/devices/electron_analyser/specs/driver_io.py +54 -21
  41. dodal/devices/electron_analyser/specs/region.py +13 -8
  42. dodal/devices/electron_analyser/types.py +15 -6
  43. dodal/devices/electron_analyser/vgscienta/detector.py +18 -8
  44. dodal/devices/electron_analyser/vgscienta/driver_io.py +62 -24
  45. dodal/devices/electron_analyser/vgscienta/region.py +33 -16
  46. dodal/devices/focusing_mirror.py +1 -1
  47. dodal/devices/i03/undulator_dcm.py +8 -3
  48. dodal/devices/i05/__init__.py +3 -0
  49. dodal/devices/i05/enums.py +8 -0
  50. dodal/devices/i09/__init__.py +2 -2
  51. dodal/devices/i09/enums.py +16 -0
  52. dodal/devices/i09_1/__init__.py +2 -2
  53. dodal/devices/i09_1/enums.py +13 -0
  54. dodal/devices/i10/mirrors.py +2 -6
  55. dodal/devices/i13_1/merlin_controller.py +1 -1
  56. dodal/devices/i19/beamstop.py +2 -2
  57. dodal/devices/i24/aperture.py +1 -1
  58. dodal/devices/motors.py +75 -1
  59. dodal/devices/oav/oav_to_redis_forwarder.py +1 -1
  60. dodal/devices/oav/pin_image_recognition/__init__.py +1 -2
  61. dodal/devices/p60/__init__.py +8 -2
  62. dodal/devices/p60/enums.py +16 -0
  63. dodal/devices/robot.py +7 -4
  64. dodal/devices/tetramm.py +1 -2
  65. dodal/devices/webcam.py +2 -1
  66. dodal/devices/zebra/zebra.py +1 -1
  67. dodal/devices/zebra/zebra_controlled_shutter.py +1 -1
  68. dodal/devices/zocalo/__init__.py +2 -0
  69. dodal/devices/zocalo/zocalo_results.py +16 -26
  70. dodal/utils.py +3 -10
  71. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/WHEEL +0 -0
  72. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/entry_points.txt +0 -0
  73. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/licenses/LICENSE +0 -0
  74. {dls_dodal-1.53.0.dist-info → dls_dodal-1.55.0.dist-info}/top_level.txt +0 -0
dodal/beamlines/i20_1.py CHANGED
@@ -1,12 +1,15 @@
1
1
  from pathlib import Path
2
2
 
3
+ from ophyd_async.epics.motor import Motor
4
+ from ophyd_async.fastcs.panda import HDFPanda
5
+
3
6
  from dodal.common.beamlines.beamline_utils import (
4
7
  device_factory,
8
+ get_path_provider,
5
9
  set_path_provider,
6
10
  )
7
11
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
8
12
  from dodal.common.visit import RemoteDirectoryServiceClient, StaticVisitPathProvider
9
- from dodal.devices.synchrotron import Synchrotron
10
13
  from dodal.devices.turbo_slit import TurboSlit
11
14
  from dodal.devices.xspress3.xspress3 import Xspress3
12
15
  from dodal.log import set_beamline as set_log_beamline
@@ -31,9 +34,14 @@ set_path_provider(
31
34
  )
32
35
  )
33
36
 
37
+ """
38
+ NOTE: Due to the CA gateway machine being switched off, PVs are not available remotely
39
+ and you need to be on the beamline network to access them.
40
+ The simplest way to do this is to `ssh i20-1-ws001` and run dodal connect i20_1 from there.
41
+ """
34
42
 
35
- # NOTE this is mock as we cannot move items on the beamline until we get sign-off to do so
36
- @device_factory(mock=True)
43
+
44
+ @device_factory()
37
45
  def turbo_slit() -> TurboSlit:
38
46
  """
39
47
  turboslit for selecting energy from the polychromator
@@ -42,18 +50,39 @@ def turbo_slit() -> TurboSlit:
42
50
  return TurboSlit(f"{PREFIX.beamline_prefix}-OP-PCHRO-01:TS:")
43
51
 
44
52
 
53
+ @device_factory()
54
+ def turbo_slit_x() -> Motor:
55
+ """
56
+ turbo slit x motor
57
+ """
58
+ return Motor(f"{PREFIX.beamline_prefix}-OP-PCHRO-01:TS:XFINE")
59
+
60
+
61
+ @device_factory()
62
+ def panda() -> HDFPanda:
63
+ return HDFPanda(
64
+ f"{PREFIX.beamline_prefix}-EA-PANDA-02:", path_provider=get_path_provider()
65
+ )
66
+
67
+
68
+ # Use mock device until motors are reconnected on the beamline
69
+ @device_factory(mock=True)
70
+ def alignment_x() -> Motor:
71
+ return Motor(f"{PREFIX.beamline_prefix}-MO-STAGE-01:X")
72
+
73
+
74
+ # Use mock device until motors are reconnected on the beamline
75
+ @device_factory(mock=True)
76
+ def alignment_y() -> Motor:
77
+ return Motor(f"{PREFIX.beamline_prefix}-MO-STAGE-01:Y")
78
+
79
+
45
80
  @device_factory(skip=True)
46
81
  def xspress3() -> Xspress3:
47
82
  """
48
83
  16 channels Xspress3 detector
49
84
  """
50
-
51
85
  return Xspress3(
52
86
  f"{PREFIX.beamline_prefix}-EA-DET-03:",
53
87
  num_channels=16,
54
88
  )
55
-
56
-
57
- @device_factory()
58
- def synchrotron() -> Synchrotron:
59
- return Synchrotron()
dodal/beamlines/i23.py CHANGED
@@ -72,7 +72,7 @@ def oav_pin_tip_detection() -> PinTipDetection:
72
72
  @device_factory()
73
73
  def shutter() -> ZebraShutter:
74
74
  """Get the i23 zebra controlled shutter."""
75
- return ZebraShutter(f"{PREFIX.beamline_prefix}-EA-SHTR-01:", "shutter")
75
+ return ZebraShutter(f"{PREFIX.beamline_prefix}-EA-SHTR-01:")
76
76
 
77
77
 
78
78
  @device_factory()
@@ -85,7 +85,6 @@ def gonio() -> SixAxisGonio:
85
85
  def zebra() -> Zebra:
86
86
  """Get the i23 zebra"""
87
87
  return Zebra(
88
- name="zebra",
89
88
  prefix=f"{PREFIX.beamline_prefix}-EA-ZEBRA-01:ZEBRA:",
90
89
  mapping=I23_ZEBRA_MAPPING,
91
90
  )
dodal/beamlines/i24.py CHANGED
@@ -51,7 +51,6 @@ def attenuator() -> ReadOnlyAttenuator:
51
51
  existing object."""
52
52
  return ReadOnlyAttenuator(
53
53
  f"{PREFIX.beamline_prefix}-OP-ATTN-01:",
54
- "attenuator",
55
54
  )
56
55
 
57
56
 
@@ -62,7 +61,6 @@ def aperture() -> Aperture:
62
61
  """
63
62
  return Aperture(
64
63
  f"{PREFIX.beamline_prefix}-AL-APTR-01:",
65
- "aperture",
66
64
  )
67
65
 
68
66
 
@@ -73,7 +71,6 @@ def beamstop() -> Beamstop:
73
71
  """
74
72
  return Beamstop(
75
73
  f"{PREFIX.beamline_prefix}-MO-BS-01:",
76
- "beamstop",
77
74
  )
78
75
 
79
76
 
@@ -84,7 +81,6 @@ def backlight() -> DualBacklight:
84
81
  """
85
82
  return DualBacklight(
86
83
  prefix=PREFIX.beamline_prefix,
87
- name="backlight",
88
84
  )
89
85
 
90
86
 
@@ -94,7 +90,6 @@ def detector_motion() -> YZStage:
94
90
  If this is called when already instantiated in i24, it will return the existing object.
95
91
  """
96
92
  return YZStage(
97
- name="detector_motion",
98
93
  prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:",
99
94
  )
100
95
 
@@ -105,7 +100,6 @@ def dcm() -> DCM:
105
100
  If this is called when already instantiated in i24, it will return the existing object.
106
101
  """
107
102
  return DCM(
108
- name="dcm",
109
103
  prefix=PREFIX.beamline_prefix,
110
104
  )
111
105
 
@@ -145,7 +139,6 @@ def pmac() -> PMAC:
145
139
  # prefix not BL but ME14E
146
140
  return PMAC(
147
141
  "ME14E-MO-CHIP-01:",
148
- "pmac",
149
142
  )
150
143
 
151
144
 
@@ -153,7 +146,6 @@ def pmac() -> PMAC:
153
146
  def oav() -> OAVBeamCentreFile:
154
147
  return OAVBeamCentreFile(
155
148
  prefix=f"{PREFIX.beamline_prefix}-DI-OAV-01:",
156
- name="oav",
157
149
  config=OAVConfigBeamCentre(ZOOM_PARAMS_FILE, DISPLAY_CONFIG),
158
150
  )
159
151
 
@@ -165,7 +157,6 @@ def vgonio() -> VerticalGoniometer:
165
157
  """
166
158
  return VerticalGoniometer(
167
159
  f"{PREFIX.beamline_prefix}-MO-VGON-01:",
168
- "vgonio",
169
160
  )
170
161
 
171
162
 
@@ -175,7 +166,6 @@ def zebra() -> Zebra:
175
166
  If this is called when already instantiated in i24, it will return the existing object.
176
167
  """
177
168
  return Zebra(
178
- name="zebra",
179
169
  prefix=f"{PREFIX.beamline_prefix}-EA-ZEBRA-01:",
180
170
  mapping=I24_ZEBRA_MAPPING,
181
171
  )
@@ -188,7 +178,6 @@ def shutter() -> HutchShutter:
188
178
  """
189
179
  return HutchShutter(
190
180
  f"{PREFIX.beamline_prefix}-PS-SHTR-01:",
191
- "shutter",
192
181
  )
193
182
 
194
183
 
@@ -197,7 +186,6 @@ def focus_mirrors() -> FocusMirrorsMode:
197
186
  """Get the i24 focus mirror devise to find the beam size."""
198
187
  return FocusMirrorsMode(
199
188
  f"{PREFIX.beamline_prefix}-OP-MFM-01:",
200
- "focus_mirrors",
201
189
  )
202
190
 
203
191
 
dodal/beamlines/p60.py CHANGED
@@ -3,7 +3,13 @@ from dodal.common.beamlines.beamline_utils import (
3
3
  )
4
4
  from dodal.common.beamlines.beamline_utils import set_beamline as set_utils_beamline
5
5
  from dodal.devices.electron_analyser.vgscienta import VGScientaAnalyserDriverIO
6
- from dodal.devices.p60 import LabXraySource, LabXraySourceReadable, LensMode
6
+ from dodal.devices.p60 import (
7
+ LabXraySource,
8
+ LabXraySourceReadable,
9
+ LensMode,
10
+ PassEnergy,
11
+ PsuMode,
12
+ )
7
13
  from dodal.log import set_beamline as set_log_beamline
8
14
  from dodal.utils import BeamlinePrefix, get_beamline_name
9
15
 
@@ -23,14 +29,18 @@ def mg_kalpha_source() -> LabXraySourceReadable:
23
29
  return LabXraySourceReadable(LabXraySource.MG_KALPHA)
24
30
 
25
31
 
32
+ # Connect will work again after this work completed
33
+ # https://jira.diamond.ac.uk/browse/P60-13
26
34
  @device_factory()
27
- def analyser_driver() -> VGScientaAnalyserDriverIO[LensMode]:
35
+ def analyser_driver() -> VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy]:
28
36
  energy_sources = {
29
37
  "source1": al_kalpha_source().energy_ev,
30
38
  "source2": mg_kalpha_source().energy_ev,
31
39
  }
32
- return VGScientaAnalyserDriverIO[LensMode](
40
+ return VGScientaAnalyserDriverIO[LensMode, PsuMode, PassEnergy](
33
41
  prefix=f"{PREFIX.beamline_prefix}-EA-DET-01:CAM:",
34
42
  lens_mode_type=LensMode,
43
+ psu_mode_type=PsuMode,
44
+ pass_energy_type=PassEnergy,
35
45
  energy_sources=energy_sources,
36
46
  )
@@ -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_34/workspace_git/gda-mx.git/configurations/i04-config/scripts/beamlineParameters",
11
+ "i04": "/dls_sw/i04/software/gda_versions/gda_9_37/workspace_git/gda-mx.git/configurations/i04-config/scripts/beamlineParameters",
12
12
  }
13
13
 
14
14
 
@@ -1,37 +1,4 @@
1
- from dodal.common.beamlines.beamline_utils import device_instantiation
2
- from dodal.devices.slits import Slits
3
- from dodal.utils import skip_device
4
-
5
1
  HDF5_SUFFIX = "HDF5:"
6
2
  TIFF_SUFFIX = "TIFF:"
7
3
  CAM_SUFFIX = "CAM:"
8
4
  DET_SUFFIX = "DET:"
9
-
10
-
11
- @skip_device()
12
- def numbered_slits(
13
- slit_number: int = 1,
14
- wait_for_connection: bool = True,
15
- fake_with_ophyd_sim: bool = False,
16
- ) -> Slits:
17
- """
18
- Create a slits object following the {beamline}-AL-SLITS-{slit_number} PV
19
- convention
20
-
21
- Args:
22
- slit_number: The number assigned to the slits in the control system, usually
23
- its position in the assembly. Defaults to 1.
24
- wait_for_connection: Require connection on instantiation. Defaults to True.
25
- fake_with_ophyd_sim: Make a fake device. Defaults to False.
26
-
27
- Returns:
28
- Slits: A new slits object
29
- """
30
-
31
- return device_instantiation(
32
- Slits,
33
- f"slits_{slit_number}",
34
- f"-AL-SLITS-{slit_number:02}:",
35
- wait_for_connection,
36
- fake_with_ophyd_sim,
37
- )
File without changes
@@ -1,10 +1,6 @@
1
- import asyncio
2
- import math
3
-
4
- from ophyd_async.core import derived_signal_rw
5
1
  from ophyd_async.epics.motor import Motor
6
2
 
7
- from dodal.devices.motors import XYZStage
3
+ from dodal.devices.motors import XYZStage, create_axis_perp_to_rotation
8
4
 
9
5
 
10
6
  class Goniometer(XYZStage):
@@ -23,26 +19,7 @@ class Goniometer(XYZStage):
23
19
  self.sampy = Motor(prefix + "SAMPY")
24
20
  self.sampz = Motor(prefix + "SAMPZ")
25
21
  self.omega = Motor(prefix + "OMEGA")
26
- self.vertical_position = derived_signal_rw(
27
- self._get,
28
- self._set,
29
- sampy=self.sampy,
30
- sampz=self.sampz,
31
- omega=self.omega,
22
+ self.vertical_position = create_axis_perp_to_rotation(
23
+ self.omega, self.sampz, self.sampy
32
24
  )
33
25
  super().__init__(name)
34
-
35
- def _get(self, sampz: float, sampy: float, omega: float) -> float:
36
- z_component = sampz * math.cos(math.radians(omega))
37
- y_component = sampy * math.sin(math.radians(omega))
38
- return z_component + y_component
39
-
40
- async def _set(self, value: float) -> None:
41
- omega = await self.omega.user_readback.get_value()
42
- z_component = value * math.cos(math.radians(omega))
43
- y_component = value * math.sin(math.radians(omega))
44
- await asyncio.gather(
45
- self.sampy.set(y_component),
46
- self.sampz.set(z_component),
47
- self.omega.set(omega),
48
- )
@@ -16,7 +16,7 @@ class LidHeatEnable(StrictEnum):
16
16
 
17
17
 
18
18
  class LaserRobot(BartRobot):
19
- def __init__(self, name: str, prefix: str) -> None:
19
+ def __init__(self, prefix: str, name: str = "") -> None:
20
20
  self.dewar_lid_heater = epics_signal_rw(
21
21
  LidHeatEnable, prefix + "DW_1_ENABLED", prefix + "DW_1_CTRL"
22
22
  )
@@ -24,4 +24,4 @@ class LaserRobot(BartRobot):
24
24
  self.set_beamline_safe = epics_signal_rw(
25
25
  ForceBit, prefix + "IP_16_FORCE_OPTION"
26
26
  )
27
- super().__init__(name=name, prefix=prefix)
27
+ super().__init__(prefix, name)
@@ -1,3 +1,3 @@
1
- from dodal.devices.b07.enums import Grating, LensMode
1
+ from dodal.devices.b07.enums import Grating, LensMode, PsuMode
2
2
 
3
- __all__ = ["Grating", "LensMode"]
3
+ __all__ = ["Grating", "LensMode", "PsuMode"]
@@ -21,4 +21,19 @@ class LensMode(SupersetEnum):
21
21
  MEDIUM_AREA = "MediumArea"
22
22
  SMALL_AREA = "SmallArea"
23
23
  HIGH_MAGNIFICATION2 = "HighMagnification2"
24
+ # This is connected to the device separately and will only have "Not connected" as
25
+ # option if disconnected. Once it is connected, "Not connected" is replaced with the
26
+ # options above. This is also why this must be a SupersetEnum.
27
+ NOT_CONNECTED = "Not connected"
28
+
29
+
30
+ class PsuMode(SupersetEnum):
31
+ V3500 = "3.5kV"
32
+ V1500 = "1.5kV"
33
+ V400 = "400V"
34
+ V100 = "100V"
35
+ V10 = "10V"
36
+ # This is connected to the device separately and will only have "Not connected" as
37
+ # option if disconnected. Once it is connected, "Not connected" is replaced with the
38
+ # options above. This is also why this must be a SupersetEnum.
24
39
  NOT_CONNECTED = "Not connected"
@@ -1,3 +1,12 @@
1
+ from dodal.devices.b07_1.ccmc import (
2
+ ChannelCutMonochromator,
3
+ ChannelCutMonochromatorPositions,
4
+ )
1
5
  from dodal.devices.b07_1.enums import Grating, LensMode
2
6
 
3
- __all__ = ["Grating", "LensMode"]
7
+ __all__ = [
8
+ "Grating",
9
+ "LensMode",
10
+ "ChannelCutMonochromator",
11
+ "ChannelCutMonochromatorPositions",
12
+ ]
@@ -0,0 +1,79 @@
1
+ from bluesky.protocols import Movable
2
+ from ophyd_async.core import (
3
+ AsyncStatus,
4
+ StandardReadable,
5
+ StrictEnum,
6
+ derived_signal_r,
7
+ )
8
+ from ophyd_async.epics.core import epics_signal_rw
9
+
10
+ from dodal.devices.motors import XYZStage
11
+
12
+
13
+ class ChannelCutMonochromatorPositions(StrictEnum):
14
+ OUT = "Out of Beam"
15
+ XTAL_2000 = "Xtal_2000"
16
+ XTAL_2250 = "Xtal_2250"
17
+ XTAL_2500 = "Xtal_2500"
18
+
19
+
20
+ ccmc_lower_limit = 1500.0
21
+ ccmc_upper_limit = 3000.0
22
+ error_message = "Can not get energy value in eV from ccmc position: "
23
+
24
+
25
+ class ChannelCutMonochromator(
26
+ StandardReadable, Movable[ChannelCutMonochromatorPositions]
27
+ ):
28
+ """
29
+ Device to move the channel cut monochromator (ccmc). CCMC has three
30
+ choices of crystal (Xtal for short). Setting energy is by means of a
31
+ multi-positioner. The positions are named after the nominal energies of the
32
+ crystals. To change energy select one of the crystals from the list.
33
+ This causes the Y motor to move that crystal into the beam and other
34
+ motors have to align the angles correctly.
35
+ """
36
+
37
+ def __init__(
38
+ self,
39
+ prefix: str,
40
+ name: str = "",
41
+ ) -> None:
42
+ """
43
+ Parameters
44
+ ----------
45
+ prefix:
46
+ Beamline specific part of the PV
47
+ name:
48
+ Name of the device
49
+ """
50
+ with self.add_children_as_readables():
51
+ # crystal motors
52
+ self._xyz = XYZStage(prefix)
53
+ # piezo motor in epics
54
+ self._y_rotation = epics_signal_rw(
55
+ float,
56
+ read_pv=prefix + "ROTY:POS:RD",
57
+ write_pv=prefix + "ROTY:MOV:WR",
58
+ )
59
+ # Must be a CHILD as read() must return this signal
60
+ self.crystal = epics_signal_rw(
61
+ ChannelCutMonochromatorPositions, prefix + "CRYSTAL:MP:SELECT"
62
+ )
63
+
64
+ # energy derived signal as property
65
+ self.energy_in_ev = derived_signal_r(
66
+ self._convert_pos_to_ev, pos_signal=self.crystal
67
+ )
68
+ super().__init__(name=name)
69
+
70
+ def _convert_pos_to_ev(self, pos_signal: ChannelCutMonochromatorPositions) -> float:
71
+ if pos_signal != ChannelCutMonochromatorPositions.OUT:
72
+ energy = float(str(pos_signal.value).split("Xtal_")[1])
73
+ if ccmc_lower_limit < energy < ccmc_upper_limit:
74
+ return energy
75
+ raise ValueError(error_message)
76
+
77
+ @AsyncStatus.wrap
78
+ async def set(self, value: ChannelCutMonochromatorPositions) -> None:
79
+ await self.crystal.set(value, wait=True)
@@ -15,4 +15,7 @@ class LensMode(SupersetEnum):
15
15
  ANGLE_RESOLVED_MODE_22 = "AngleResolvedMode22"
16
16
  ANGLE_RESOLVED_MODE_30 = "AngleResolvedMode30"
17
17
  LARGE_AREA = "LargeArea"
18
+ # This is connected to the device separately and will only have "Not connected" as
19
+ # option if disconnected. Once it is connected, "Not connected" is replaced with the
20
+ # options above. This is also why this must be a SupersetEnum.
18
21
  NOT_CONNECTED = "Not connected"
@@ -1,4 +1,3 @@
1
- import asyncio
2
1
  from abc import ABC, abstractmethod
3
2
  from collections.abc import Mapping
4
3
  from typing import Generic, TypeVar
@@ -19,11 +18,15 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
19
18
 
20
19
  from dodal.devices.electron_analyser.abstract.base_region import (
21
20
  TAbstractBaseRegion,
21
+ )
22
+ from dodal.devices.electron_analyser.abstract.types import (
22
23
  TAcquisitionMode,
23
24
  TLensMode,
25
+ TPassEnergy,
26
+ TPsuMode,
24
27
  )
25
28
  from dodal.devices.electron_analyser.enums import EnergyMode
26
- from dodal.devices.electron_analyser.util import to_binding_energy, to_kinetic_energy
29
+ from dodal.devices.electron_analyser.util import to_binding_energy
27
30
 
28
31
 
29
32
  class AbstractAnalyserDriverIO(
@@ -31,11 +34,12 @@ class AbstractAnalyserDriverIO(
31
34
  StandardReadable,
32
35
  ADBaseIO,
33
36
  Movable[TAbstractBaseRegion],
34
- Generic[TAbstractBaseRegion, TAcquisitionMode, TLensMode],
37
+ Generic[TAbstractBaseRegion, TAcquisitionMode, TLensMode, TPsuMode, TPassEnergy],
35
38
  ):
36
39
  """
37
- Generic device to configure electron analyser with new region settings.
38
- Electron analysers should inherit from this class for further specialisation.
40
+ Driver device that defines signals and readables that should be common to all
41
+ electron analysers. Implementations of electron analyser devices should inherit
42
+ from this class and define additional specialised signals and methods.
39
43
  """
40
44
 
41
45
  def __init__(
@@ -43,6 +47,8 @@ class AbstractAnalyserDriverIO(
43
47
  prefix: str,
44
48
  acquisition_mode_type: type[TAcquisitionMode],
45
49
  lens_mode_type: type[TLensMode],
50
+ psu_mode_type: type[TPsuMode],
51
+ pass_energy_type: type[TPassEnergy],
46
52
  energy_sources: Mapping[str, SignalR[float]],
47
53
  name: str = "",
48
54
  ) -> None:
@@ -55,6 +61,10 @@ class AbstractAnalyserDriverIO(
55
61
  for this device.
56
62
  lens_mode_type: Enum that determines the available lens mode for this
57
63
  device.
64
+ psu_mode_type: Enum that determines the available psu modes for this device.
65
+ pass_energy_type: Can be enum or float, depends on electron analyser model.
66
+ If enum, it determines the available pass energies for
67
+ this device.
58
68
  energy_sources: Map that pairs a source name to an energy value signal
59
69
  (in eV).
60
70
  name: Name of the device.
@@ -62,6 +72,8 @@ class AbstractAnalyserDriverIO(
62
72
  self.energy_sources = energy_sources
63
73
  self.acquisition_mode_type = acquisition_mode_type
64
74
  self.lens_mode_type = lens_mode_type
75
+ self.psu_mode_type = psu_mode_type
76
+ self.pass_energy_type = pass_energy_type
65
77
 
66
78
  with self.add_children_as_readables():
67
79
  self.image = epics_signal_r(Array1D[np.float64], prefix + "IMAGE")
@@ -78,18 +90,20 @@ class AbstractAnalyserDriverIO(
78
90
  EnergyMode, initial_value=EnergyMode.KINETIC
79
91
  )
80
92
  self.low_energy = epics_signal_rw(float, prefix + "LOW_ENERGY")
93
+ self.centre_energy = epics_signal_rw(float, prefix + "CENTRE_ENERGY")
81
94
  self.high_energy = epics_signal_rw(float, prefix + "HIGH_ENERGY")
82
95
  self.slices = epics_signal_rw(int, prefix + "SLICES")
83
96
  self.lens_mode = epics_signal_rw(lens_mode_type, prefix + "LENS_MODE")
84
- self.pass_energy = epics_signal_rw(
85
- self.pass_energy_type, prefix + "PASS_ENERGY"
86
- )
97
+ self.pass_energy = epics_signal_rw(pass_energy_type, prefix + "PASS_ENERGY")
87
98
  self.energy_step = epics_signal_rw(float, prefix + "STEP_SIZE")
88
99
  self.iterations = epics_signal_rw(int, prefix + "NumExposures")
89
100
  self.acquisition_mode = epics_signal_rw(
90
101
  acquisition_mode_type, prefix + "ACQ_MODE"
91
102
  )
92
103
  self.excitation_energy_source = soft_signal_rw(str, initial_value="")
104
+ # This is used by each electron analyser, however it depends on the electron
105
+ # analyser type to know if is moved with region settings.
106
+ self.psu_mode = epics_signal_rw(psu_mode_type, prefix + "PSU_MODE")
93
107
 
94
108
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
95
109
  # Read once per scan after data acquired
@@ -114,42 +128,17 @@ class AbstractAnalyserDriverIO(
114
128
 
115
129
  super().__init__(prefix=prefix, name=name)
116
130
 
131
+ @abstractmethod
117
132
  @AsyncStatus.wrap
118
133
  async def set(self, region: TAbstractBaseRegion):
119
134
  """
120
- This should encompass all core region logic which is common to every electron
121
- analyser for setting up the driver.
135
+ Move a group of signals defined in a region. Each implementation of this class
136
+ is responsible for implementing this method correctly.
122
137
 
123
138
  Args:
124
139
  region: Contains the parameters to setup the driver for a scan.
125
140
  """
126
141
 
127
- source = self._get_energy_source(region.excitation_energy_source)
128
- excitation_energy = await source.get_value() # eV
129
-
130
- pass_energy_type = self.pass_energy_type
131
- pass_energy = pass_energy_type(region.pass_energy)
132
-
133
- low_energy = to_kinetic_energy(
134
- region.low_energy, region.energy_mode, excitation_energy
135
- )
136
- high_energy = to_kinetic_energy(
137
- region.high_energy, region.energy_mode, excitation_energy
138
- )
139
- await asyncio.gather(
140
- self.region_name.set(region.name),
141
- self.energy_mode.set(region.energy_mode),
142
- self.low_energy.set(low_energy),
143
- self.high_energy.set(high_energy),
144
- self.slices.set(region.slices),
145
- self.lens_mode.set(region.lens_mode),
146
- self.pass_energy.set(pass_energy),
147
- self.iterations.set(region.iterations),
148
- self.acquisition_mode.set(region.acquisition_mode),
149
- self.excitation_energy.set(excitation_energy),
150
- self.excitation_energy_source.set(source.name),
151
- )
152
-
153
142
  def _get_energy_source(self, alias_name: str) -> SignalR[float]:
154
143
  energy_source = self.energy_sources.get(alias_name)
155
144
  if energy_source is None:
@@ -233,18 +222,6 @@ class AbstractAnalyserDriverIO(
233
222
  def _calculate_total_intensity(self, spectrum: Array1D[np.float64]) -> float:
234
223
  return float(np.sum(spectrum, dtype=np.float64))
235
224
 
236
- @property
237
- @abstractmethod
238
- def pass_energy_type(self) -> type:
239
- """
240
- Return the type the pass_energy should be. Depends on underlying analyser
241
- software.
242
-
243
- Returns:
244
- Type the pass energy parameter from a region needs to be cast to so it can
245
- be set correctly on the signal.
246
- """
247
-
248
225
 
249
226
  TAbstractAnalyserDriverIO = TypeVar(
250
227
  "TAbstractAnalyserDriverIO", bound=AbstractAnalyserDriverIO
@@ -3,9 +3,13 @@ from abc import ABC
3
3
  from collections.abc import Callable
4
4
  from typing import Generic, TypeVar
5
5
 
6
- from ophyd_async.core import StrictEnum, SupersetEnum
7
6
  from pydantic import BaseModel, Field, model_validator
8
7
 
8
+ from dodal.devices.electron_analyser.abstract.types import (
9
+ TAcquisitionMode,
10
+ TLensMode,
11
+ TPassEnergy,
12
+ )
9
13
  from dodal.devices.electron_analyser.enums import EnergyMode
10
14
 
11
15
 
@@ -44,17 +48,10 @@ def energy_mode_validation(data: dict) -> dict:
44
48
  return data
45
49
 
46
50
 
47
- TAcquisitionMode = TypeVar("TAcquisitionMode", bound=StrictEnum)
48
- # Allow SupersetEnum. Specs analysers can connect to Lens mode separately to the
49
- # analyser which leaves the enum to either be "Not connected" OR the available enums
50
- # when connected.
51
- TLensMode = TypeVar("TLensMode", bound=SupersetEnum | StrictEnum)
52
-
53
-
54
51
  class AbstractBaseRegion(
55
52
  ABC,
56
53
  JavaToPythonModel,
57
- Generic[TAcquisitionMode, TLensMode],
54
+ Generic[TAcquisitionMode, TLensMode, TPassEnergy],
58
55
  ):
59
56
  """
60
57
  Generic region model that holds the data. Specialised region models should inherit
@@ -68,9 +65,10 @@ class AbstractBaseRegion(
68
65
  excitation_energy_source: str = "source1"
69
66
  # These ones we need subclasses to provide default values
70
67
  lens_mode: TLensMode
71
- pass_energy: int
68
+ pass_energy: TPassEnergy
72
69
  acquisition_mode: TAcquisitionMode
73
70
  low_energy: float
71
+ centre_energy: float
74
72
  high_energy: float
75
73
  step_time: float
76
74
  energy_step: float # in eV
@@ -95,7 +93,7 @@ TAbstractBaseRegion = TypeVar("TAbstractBaseRegion", bound=AbstractBaseRegion)
95
93
  class AbstractBaseSequence(
96
94
  ABC,
97
95
  JavaToPythonModel,
98
- Generic[TAbstractBaseRegion, TLensMode],
96
+ Generic[TAbstractBaseRegion],
99
97
  ):
100
98
  """
101
99
  Generic sequence model that holds the list of region data. Specialised sequence