dls-dodal 1.66.0__py3-none-any.whl → 1.68.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 (83) hide show
  1. {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/RECORD +75 -65
  3. dodal/_version.py +2 -2
  4. dodal/beamlines/b07.py +1 -1
  5. dodal/beamlines/b07_1.py +1 -1
  6. dodal/beamlines/i03.py +92 -208
  7. dodal/beamlines/i04.py +22 -1
  8. dodal/beamlines/i05.py +1 -1
  9. dodal/beamlines/i06.py +1 -1
  10. dodal/beamlines/i09.py +1 -1
  11. dodal/beamlines/i09_1.py +27 -3
  12. dodal/beamlines/i09_2.py +58 -2
  13. dodal/beamlines/i10_optics.py +44 -25
  14. dodal/beamlines/i16.py +23 -0
  15. dodal/beamlines/i17.py +7 -3
  16. dodal/beamlines/i19_1.py +26 -14
  17. dodal/beamlines/i19_2.py +49 -38
  18. dodal/beamlines/i21.py +61 -2
  19. dodal/beamlines/i22.py +16 -1
  20. dodal/beamlines/p60.py +1 -1
  21. dodal/beamlines/training_rig.py +0 -16
  22. dodal/cli.py +26 -12
  23. dodal/common/coordination.py +3 -2
  24. dodal/device_manager.py +604 -0
  25. dodal/devices/cryostream.py +28 -57
  26. dodal/devices/eiger.py +41 -27
  27. dodal/devices/electron_analyser/__init__.py +0 -33
  28. dodal/devices/electron_analyser/base/__init__.py +58 -0
  29. dodal/devices/electron_analyser/base/base_controller.py +73 -0
  30. dodal/devices/electron_analyser/base/base_detector.py +214 -0
  31. dodal/devices/electron_analyser/{abstract → base}/base_driver_io.py +23 -42
  32. dodal/devices/electron_analyser/{abstract → base}/base_region.py +47 -11
  33. dodal/devices/electron_analyser/{util.py → base/base_util.py} +1 -1
  34. dodal/devices/electron_analyser/{energy_sources.py → base/energy_sources.py} +1 -1
  35. dodal/devices/electron_analyser/specs/__init__.py +4 -4
  36. dodal/devices/electron_analyser/specs/specs_detector.py +46 -0
  37. dodal/devices/electron_analyser/specs/{driver_io.py → specs_driver_io.py} +23 -26
  38. dodal/devices/electron_analyser/specs/{region.py → specs_region.py} +4 -3
  39. dodal/devices/electron_analyser/vgscienta/__init__.py +4 -4
  40. dodal/devices/electron_analyser/vgscienta/vgscienta_detector.py +52 -0
  41. dodal/devices/electron_analyser/vgscienta/{driver_io.py → vgscienta_driver_io.py} +25 -31
  42. dodal/devices/electron_analyser/vgscienta/{region.py → vgscienta_region.py} +6 -6
  43. dodal/devices/i04/max_pixel.py +38 -0
  44. dodal/devices/i09_1_shared/__init__.py +8 -1
  45. dodal/devices/i09_1_shared/hard_energy.py +112 -0
  46. dodal/devices/i09_2_shared/__init__.py +0 -0
  47. dodal/devices/i09_2_shared/i09_apple2.py +14 -0
  48. dodal/devices/i10/i10_apple2.py +24 -22
  49. dodal/devices/i17/i17_apple2.py +32 -20
  50. dodal/devices/i19/access_controlled/attenuator_motor_squad.py +61 -0
  51. dodal/devices/i19/access_controlled/blueapi_device.py +9 -1
  52. dodal/devices/i19/access_controlled/shutter.py +2 -4
  53. dodal/devices/i21/__init__.py +3 -1
  54. dodal/devices/insertion_device/__init__.py +58 -0
  55. dodal/devices/{apple2_undulator.py → insertion_device/apple2_undulator.py} +102 -44
  56. dodal/devices/insertion_device/energy_motor_lookup.py +88 -0
  57. dodal/devices/insertion_device/id_enum.py +17 -0
  58. dodal/devices/insertion_device/lookup_table_models.py +317 -0
  59. dodal/devices/motors.py +14 -0
  60. dodal/devices/robot.py +16 -11
  61. dodal/plans/__init__.py +1 -1
  62. dodal/plans/configure_arm_trigger_and_disarm_detector.py +2 -4
  63. dodal/testing/electron_analyser/device_factory.py +4 -4
  64. dodal/testing/fixtures/devices/__init__.py +0 -0
  65. dodal/testing/fixtures/devices/apple2.py +78 -0
  66. dodal/testing/fixtures/run_engine.py +4 -0
  67. dodal/utils.py +6 -3
  68. dodal/devices/electron_analyser/abstract/__init__.py +0 -25
  69. dodal/devices/electron_analyser/abstract/base_detector.py +0 -63
  70. dodal/devices/electron_analyser/abstract/types.py +0 -12
  71. dodal/devices/electron_analyser/detector.py +0 -143
  72. dodal/devices/electron_analyser/specs/detector.py +0 -34
  73. dodal/devices/electron_analyser/types.py +0 -57
  74. dodal/devices/electron_analyser/vgscienta/detector.py +0 -48
  75. dodal/devices/util/lookup_tables_apple2.py +0 -390
  76. {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/WHEEL +0 -0
  77. {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/entry_points.txt +0 -0
  78. {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/licenses/LICENSE +0 -0
  79. {dls_dodal-1.66.0.dist-info → dls_dodal-1.68.0.dist-info}/top_level.txt +0 -0
  80. /dodal/devices/electron_analyser/{enums.py → base/base_enums.py} +0 -0
  81. /dodal/devices/electron_analyser/specs/{enums.py → specs_enums.py} +0 -0
  82. /dodal/devices/electron_analyser/vgscienta/{enums.py → vgscienta_enums.py} +0 -0
  83. /dodal/plans/{scanspec.py → spec_path.py} +0 -0
@@ -0,0 +1,214 @@
1
+ from typing import Generic, TypeVar
2
+
3
+ from bluesky.protocols import Reading, Stageable, Triggerable
4
+ from event_model import DataKey
5
+ from ophyd_async.core import (
6
+ AsyncConfigurable,
7
+ AsyncReadable,
8
+ AsyncStatus,
9
+ Device,
10
+ TriggerInfo,
11
+ )
12
+
13
+ from dodal.common.data_util import load_json_file_to_class
14
+ from dodal.devices.electron_analyser.base.base_controller import (
15
+ ElectronAnalyserController,
16
+ )
17
+ from dodal.devices.electron_analyser.base.base_driver_io import (
18
+ GenericAnalyserDriverIO,
19
+ TAbstractAnalyserDriverIO,
20
+ )
21
+ from dodal.devices.electron_analyser.base.base_region import (
22
+ GenericRegion,
23
+ GenericSequence,
24
+ TAbstractBaseRegion,
25
+ TAbstractBaseSequence,
26
+ )
27
+
28
+
29
+ class BaseElectronAnalyserDetector(
30
+ Device,
31
+ Triggerable,
32
+ AsyncReadable,
33
+ AsyncConfigurable,
34
+ Generic[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
35
+ ):
36
+ """
37
+ Detector for data acquisition of electron analyser. Can only acquire using settings
38
+ already configured for the device.
39
+
40
+ If possible, this should be changed to inherit from a StandardDetector. Currently,
41
+ StandardDetector forces you to use a file writer which doesn't apply here.
42
+ See issue https://github.com/bluesky/ophyd-async/issues/888
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ controller: ElectronAnalyserController[
48
+ TAbstractAnalyserDriverIO, TAbstractBaseRegion
49
+ ],
50
+ name: str = "",
51
+ ):
52
+ self._controller = controller
53
+ super().__init__(name)
54
+
55
+ @AsyncStatus.wrap
56
+ async def set(self, region: TAbstractBaseRegion) -> None:
57
+ await self._controller.setup_with_region(region)
58
+
59
+ @AsyncStatus.wrap
60
+ async def trigger(self) -> None:
61
+ await self._controller.prepare(TriggerInfo())
62
+ await self._controller.arm()
63
+ await self._controller.wait_for_idle()
64
+
65
+ async def read(self) -> dict[str, Reading]:
66
+ return await self._controller.driver.read()
67
+
68
+ async def describe(self) -> dict[str, DataKey]:
69
+ data = await self._controller.driver.describe()
70
+ # Correct the shape for image
71
+ prefix = self._controller.driver.name + "-"
72
+ energy_size = len(await self._controller.driver.energy_axis.get_value())
73
+ angle_size = len(await self._controller.driver.angle_axis.get_value())
74
+ data[prefix + "image"]["shape"] = [angle_size, energy_size]
75
+ return data
76
+
77
+ async def read_configuration(self) -> dict[str, Reading]:
78
+ return await self._controller.driver.read_configuration()
79
+
80
+ async def describe_configuration(self) -> dict[str, DataKey]:
81
+ return await self._controller.driver.describe_configuration()
82
+
83
+
84
+ GenericBaseElectronAnalyserDetector = BaseElectronAnalyserDetector[
85
+ GenericAnalyserDriverIO, GenericRegion
86
+ ]
87
+
88
+
89
+ class ElectronAnalyserRegionDetector(
90
+ BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
91
+ Generic[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
92
+ ):
93
+ """
94
+ Extends electron analyser detector to configure specific region settings before data
95
+ acquisition. It is designed to only exist inside a plan.
96
+ """
97
+
98
+ def __init__(
99
+ self,
100
+ controller: ElectronAnalyserController[
101
+ TAbstractAnalyserDriverIO, TAbstractBaseRegion
102
+ ],
103
+ region: TAbstractBaseRegion,
104
+ name: str = "",
105
+ ):
106
+ self.region = region
107
+ super().__init__(controller, name)
108
+
109
+ @AsyncStatus.wrap
110
+ async def trigger(self) -> None:
111
+ # Configure region parameters on the driver first before data collection.
112
+ await self.set(self.region)
113
+ await super().trigger()
114
+
115
+
116
+ GenericElectronAnalyserRegionDetector = ElectronAnalyserRegionDetector[
117
+ GenericAnalyserDriverIO, GenericRegion
118
+ ]
119
+ TElectronAnalyserRegionDetector = TypeVar(
120
+ "TElectronAnalyserRegionDetector",
121
+ bound=ElectronAnalyserRegionDetector,
122
+ )
123
+
124
+
125
+ class ElectronAnalyserDetector(
126
+ BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
127
+ Stageable,
128
+ Generic[TAbstractBaseSequence, TAbstractAnalyserDriverIO, TAbstractBaseRegion],
129
+ ):
130
+ """
131
+ Electron analyser detector with the additional functionality to load a sequence file
132
+ and create a list of temporary ElectronAnalyserRegionDetector objects. These will
133
+ setup configured region settings before data acquisition.
134
+ """
135
+
136
+ def __init__(
137
+ self,
138
+ sequence_class: type[TAbstractBaseSequence],
139
+ controller: ElectronAnalyserController[
140
+ TAbstractAnalyserDriverIO, TAbstractBaseRegion
141
+ ],
142
+ name: str = "",
143
+ ):
144
+ self._sequence_class = sequence_class
145
+ super().__init__(controller, name)
146
+
147
+ @AsyncStatus.wrap
148
+ async def stage(self) -> None:
149
+ """
150
+ Prepare the detector for use by ensuring it is idle and ready.
151
+
152
+ This method asynchronously stages the detector by first disarming the controller
153
+ to ensure the detector is not actively acquiring data, then invokes the driver's
154
+ stage procedure. This ensures the detector is in a known, ready state
155
+ before use.
156
+
157
+ Raises:
158
+ Any exceptions raised by the driver's stage or controller's disarm methods.
159
+ """
160
+ await self._controller.disarm()
161
+
162
+ @AsyncStatus.wrap
163
+ async def unstage(self) -> None:
164
+ """Disarm the detector."""
165
+ await self._controller.disarm()
166
+
167
+ def load_sequence(self, filename: str) -> TAbstractBaseSequence:
168
+ """
169
+ Load the sequence data from a provided json file into a sequence class.
170
+
171
+ Args:
172
+ filename: Path to the sequence file containing the region data.
173
+
174
+ Returns:
175
+ Pydantic model representing the sequence file.
176
+ """
177
+ return load_json_file_to_class(self._sequence_class, filename)
178
+
179
+ def create_region_detector_list(
180
+ self, filename: str, enabled_only=True
181
+ ) -> list[
182
+ ElectronAnalyserRegionDetector[TAbstractAnalyserDriverIO, TAbstractBaseRegion]
183
+ ]:
184
+ """
185
+ Create a list of detectors equal to the number of regions in a sequence file.
186
+ Each detector is responsible for setting up a specific region.
187
+
188
+ Args:
189
+ filename: Path to the sequence file containing the region data.
190
+ enabled_only: If true, only include the region if enabled is True.
191
+
192
+ Returns:
193
+ List of ElectronAnalyserRegionDetector, equal to the number of regions in
194
+ the sequence file.
195
+ """
196
+ seq = self.load_sequence(filename)
197
+ regions: list[TAbstractBaseRegion] = (
198
+ seq.get_enabled_regions() if enabled_only else seq.regions
199
+ )
200
+ return [
201
+ ElectronAnalyserRegionDetector[
202
+ TAbstractAnalyserDriverIO, TAbstractBaseRegion
203
+ ](self._controller, r, self.name + "_" + r.name)
204
+ for r in regions
205
+ ]
206
+
207
+
208
+ GenericElectronAnalyserDetector = ElectronAnalyserDetector[
209
+ GenericSequence, GenericAnalyserDriverIO, GenericRegion
210
+ ]
211
+ TElectronAnalyserDetector = TypeVar(
212
+ "TElectronAnalyserDetector",
213
+ bound=ElectronAnalyserDetector,
214
+ )
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Generic, TypeVar
2
+ from typing import Generic, TypeAlias, TypeVar
3
3
 
4
4
  import numpy as np
5
5
  from bluesky.protocols import Movable
@@ -9,27 +9,29 @@ from ophyd_async.core import (
9
9
  SignalR,
10
10
  StandardReadable,
11
11
  StandardReadableFormat,
12
+ StrictEnum,
13
+ SupersetEnum,
12
14
  derived_signal_r,
13
15
  soft_signal_rw,
14
16
  )
15
- from ophyd_async.epics.adcore import ADBaseIO, ADImageMode
17
+ from ophyd_async.epics.adcore import ADBaseIO
16
18
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
17
19
 
18
- from dodal.devices.electron_analyser.abstract.base_region import (
20
+ from dodal.devices.electron_analyser.base.base_enums import EnergyMode
21
+ from dodal.devices.electron_analyser.base.base_region import (
22
+ AnyAcqMode,
23
+ AnyLensMode,
24
+ AnyPassEnergy,
25
+ GenericRegion,
19
26
  TAbstractBaseRegion,
20
- )
21
- from dodal.devices.electron_analyser.abstract.types import (
22
27
  TAcquisitionMode,
23
28
  TLensMode,
24
29
  TPassEnergy,
25
- TPsuMode,
26
- )
27
- from dodal.devices.electron_analyser.energy_sources import (
28
- DualEnergySource,
29
- EnergySource,
30
30
  )
31
- from dodal.devices.electron_analyser.enums import EnergyMode
32
- from dodal.devices.electron_analyser.util import to_binding_energy
31
+ from dodal.devices.electron_analyser.base.base_util import to_binding_energy
32
+
33
+ AnyPsuMode: TypeAlias = SupersetEnum | StrictEnum
34
+ TPsuMode = TypeVar("TPsuMode", bound=AnyPsuMode)
33
35
 
34
36
 
35
37
  class AbstractAnalyserDriverIO(
@@ -52,7 +54,6 @@ class AbstractAnalyserDriverIO(
52
54
  lens_mode_type: type[TLensMode],
53
55
  psu_mode_type: type[TPsuMode],
54
56
  pass_energy_type: type[TPassEnergy],
55
- energy_source: EnergySource | DualEnergySource,
56
57
  name: str = "",
57
58
  ) -> None:
58
59
  """
@@ -87,7 +88,6 @@ class AbstractAnalyserDriverIO(
87
88
  self.total_intensity = derived_signal_r(
88
89
  self._calculate_total_intensity, spectrum=self.spectrum
89
90
  )
90
- self.energy_source = energy_source
91
91
 
92
92
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
93
93
  # Read once per scan after data acquired
@@ -96,6 +96,9 @@ class AbstractAnalyserDriverIO(
96
96
  self.energy_mode = soft_signal_rw(
97
97
  EnergyMode, initial_value=EnergyMode.KINETIC
98
98
  )
99
+ self.cached_excitation_energy = soft_signal_rw(
100
+ float, initial_value=0, units="eV"
101
+ )
99
102
  self.low_energy = epics_signal_rw(float, prefix + "LOW_ENERGY")
100
103
  self.centre_energy = epics_signal_rw(float, prefix + "CENTRE_ENERGY")
101
104
  self.high_energy = epics_signal_rw(float, prefix + "HIGH_ENERGY")
@@ -121,7 +124,7 @@ class AbstractAnalyserDriverIO(
121
124
  self._calculate_binding_energy_axis,
122
125
  "eV",
123
126
  energy_axis=self.energy_axis,
124
- excitation_energy=self.energy_source.energy,
127
+ excitation_energy=self.cached_excitation_energy,
125
128
  energy_mode=self.energy_mode,
126
129
  )
127
130
  self.angle_axis = self._create_angle_axis_signal(prefix)
@@ -134,34 +137,9 @@ class AbstractAnalyserDriverIO(
134
137
  iterations=self.iterations,
135
138
  )
136
139
 
137
- @AsyncStatus.wrap
138
- async def stage(self) -> None:
139
- await self.image_mode.set(ADImageMode.SINGLE)
140
- await super().stage()
141
-
142
- @AsyncStatus.wrap
143
- async def set(self, region: TAbstractBaseRegion):
144
- """
145
- Take a region object and setup the driver with it. If using a DualEnergySource,
146
- set it to use the source selected by the region. It also converts the region to
147
- kinetic mode before we move the driver signals to the region parameter values:
148
-
149
- Args:
150
- region: Contains the parameters to setup the driver for a scan.
151
- """
152
- if isinstance(self.energy_source, DualEnergySource):
153
- self.energy_source.selected_source.set(region.excitation_energy_source)
154
- excitation_energy = await self.energy_source.energy.get_value()
155
-
156
- # Switch to kinetic energy as epics doesn't support BINDING.
157
- ke_region = region.switch_energy_mode(EnergyMode.KINETIC, excitation_energy)
158
- await self._set_region(ke_region)
159
- # Set the true energy mode from original region so binding_energy_axis can be
160
- # calculated correctly.
161
- await self.energy_mode.set(region.energy_mode)
162
-
163
140
  @abstractmethod
164
- async def _set_region(self, ke_region: TAbstractBaseRegion):
141
+ @AsyncStatus.wrap
142
+ async def set(self, epics_region: TAbstractBaseRegion):
165
143
  """
166
144
  Move a group of signals defined in a region. Each implementation of this class
167
145
  is responsible for implementing this method correctly.
@@ -245,6 +223,9 @@ class AbstractAnalyserDriverIO(
245
223
  return float(np.sum(spectrum, dtype=np.float64))
246
224
 
247
225
 
226
+ GenericAnalyserDriverIO = AbstractAnalyserDriverIO[
227
+ GenericRegion, AnyAcqMode, AnyLensMode, AnyPsuMode, AnyPassEnergy
228
+ ]
248
229
  TAbstractAnalyserDriverIO = TypeVar(
249
230
  "TAbstractAnalyserDriverIO", bound=AbstractAnalyserDriverIO
250
231
  )
@@ -1,17 +1,29 @@
1
1
  import re
2
2
  from abc import ABC
3
3
  from collections.abc import Callable
4
- from typing import Generic, Self, TypeVar
4
+ from typing import Generic, Self, TypeAlias, TypeVar
5
5
 
6
+ from ophyd_async.core import StrictEnum, SupersetEnum
6
7
  from pydantic import BaseModel, Field, model_validator
7
8
 
8
- from dodal.devices.electron_analyser.abstract.types import (
9
- TAcquisitionMode,
10
- TLensMode,
11
- TPassEnergy,
9
+ from dodal.devices.electron_analyser.base.base_enums import EnergyMode, SelectedSource
10
+ from dodal.devices.electron_analyser.base.base_util import (
11
+ to_binding_energy,
12
+ to_kinetic_energy,
12
13
  )
13
- from dodal.devices.electron_analyser.enums import EnergyMode, SelectedSource
14
- from dodal.devices.electron_analyser.util import to_binding_energy, to_kinetic_energy
14
+
15
+ AnyAcqMode: TypeAlias = StrictEnum
16
+ AnyLensMode: TypeAlias = SupersetEnum | StrictEnum
17
+ AnyPassEnergy: TypeAlias = StrictEnum | float
18
+ AnyPsuMode: TypeAlias = SupersetEnum | StrictEnum
19
+
20
+ TAcquisitionMode = TypeVar("TAcquisitionMode", bound=AnyAcqMode)
21
+ # Allow SupersetEnum. Specs analysers can connect to Lens and Psu mode separately to the
22
+ # analyser which leaves the enum to either be "Not connected" OR the available enums
23
+ # when connected.
24
+ TLensMode = TypeVar("TLensMode", bound=AnyLensMode)
25
+ TPassEnergy = TypeVar("TPassEnergy", bound=AnyPassEnergy)
26
+ TPsuMode = TypeVar("TPsuMode", bound=AnyPsuMode)
15
27
 
16
28
 
17
29
  def java_to_python_case(java_str: str) -> str:
@@ -88,10 +100,13 @@ class AbstractBaseRegion(
88
100
  return self.energy_mode == EnergyMode.KINETIC
89
101
 
90
102
  def switch_energy_mode(
91
- self, energy_mode: EnergyMode, excitation_energy: float, copy: bool = True
103
+ self,
104
+ energy_mode: EnergyMode,
105
+ excitation_energy: float,
106
+ copy: bool = True,
92
107
  ) -> Self:
93
108
  """
94
- Switch region with to a new energy mode with a new energy mode: Kinetic or Binding.
109
+ Get a region with a new energy mode: Kinetic or Binding.
95
110
  It caculates new values for low_energy, centre_energy, high_energy, via the
96
111
  excitation enerrgy. It doesn't calculate anything if the region is already of
97
112
  the same energy mode.
@@ -100,8 +115,8 @@ class AbstractBaseRegion(
100
115
  energy_mode: Mode you want to switch the region to.
101
116
  excitation_energy: Energy conversion for low_energy, centre_energy, and
102
117
  high_energy for new energy mode.
103
- copy: Defaults to True. If true, create a copy of this region for the new
104
- energy_mode and return it. If False, alter this region for the
118
+ copy: Defaults to True. If true, create a copy of this region to alter for
119
+ the new energy_mode and return it. If False, alter this region for the
105
120
  energy_mode and return it self.
106
121
 
107
122
  Returns:
@@ -126,6 +141,25 @@ class AbstractBaseRegion(
126
141
 
127
142
  return switched_r
128
143
 
144
+ def prepare_for_epics(self, excitation_energy: float, copy: bool = True) -> Self:
145
+ """Prepares a region for epics by converting BINDING to KINETIC by calculating
146
+ new values for low_energy, centre_energy, and high_energy while also preserving
147
+ the original energy mode e.g mode BINDING will stay as BINDING.
148
+
149
+ Parameters:
150
+ excitation_energy: Energy conversion for low_energy, centre_energy, and
151
+ high_energy for new energy mode.
152
+ copy: Defaults to True. If true, create a copy of this region to alter to
153
+ calculate new energy values to return. If false, alter this region.
154
+ Returns:
155
+ Region with selected original energy mode and new calculated KINETIC energy
156
+ values for epics.
157
+ """
158
+ original_energy_mode = self.energy_mode
159
+ r = self.switch_energy_mode(EnergyMode.KINETIC, excitation_energy, copy)
160
+ r.energy_mode = original_energy_mode
161
+ return r
162
+
129
163
  @model_validator(mode="before")
130
164
  @classmethod
131
165
  def before_validation(cls, data: dict) -> dict:
@@ -133,6 +167,7 @@ class AbstractBaseRegion(
133
167
  return energy_mode_validation(data)
134
168
 
135
169
 
170
+ GenericRegion = AbstractBaseRegion[AnyAcqMode, AnyLensMode, AnyPassEnergy]
136
171
  TAbstractBaseRegion = TypeVar("TAbstractBaseRegion", bound=AbstractBaseRegion)
137
172
 
138
173
 
@@ -163,4 +198,5 @@ class AbstractBaseSequence(
163
198
  return next((region for region in self.regions if region.name == name), None)
164
199
 
165
200
 
201
+ GenericSequence = AbstractBaseSequence[GenericRegion]
166
202
  TAbstractBaseSequence = TypeVar("TAbstractBaseSequence", bound=AbstractBaseSequence)
@@ -1,4 +1,4 @@
1
- from dodal.devices.electron_analyser.enums import EnergyMode
1
+ from dodal.devices.electron_analyser.base.base_enums import EnergyMode
2
2
 
3
3
 
4
4
  def to_kinetic_energy(
@@ -10,7 +10,7 @@ from ophyd_async.core import (
10
10
  soft_signal_rw,
11
11
  )
12
12
 
13
- from dodal.devices.electron_analyser.enums import SelectedSource
13
+ from dodal.devices.electron_analyser.base.base_enums import SelectedSource
14
14
 
15
15
 
16
16
  class AbstractEnergySource(StandardReadable):
@@ -1,7 +1,7 @@
1
- from .detector import SpecsDetector
2
- from .driver_io import SpecsAnalyserDriverIO
3
- from .enums import AcquisitionMode
4
- from .region import SpecsRegion, SpecsSequence
1
+ from .specs_detector import SpecsDetector
2
+ from .specs_driver_io import SpecsAnalyserDriverIO
3
+ from .specs_enums import AcquisitionMode
4
+ from .specs_region import SpecsRegion, SpecsSequence
5
5
 
6
6
  __all__ = [
7
7
  "SpecsDetector",
@@ -0,0 +1,46 @@
1
+ from typing import Generic
2
+
3
+ from dodal.devices.electron_analyser.base.base_controller import (
4
+ ElectronAnalyserController,
5
+ )
6
+ from dodal.devices.electron_analyser.base.base_detector import ElectronAnalyserDetector
7
+ from dodal.devices.electron_analyser.base.base_region import TLensMode, TPsuMode
8
+ from dodal.devices.electron_analyser.base.energy_sources import (
9
+ DualEnergySource,
10
+ EnergySource,
11
+ )
12
+ from dodal.devices.electron_analyser.specs.specs_driver_io import SpecsAnalyserDriverIO
13
+ from dodal.devices.electron_analyser.specs.specs_region import (
14
+ SpecsRegion,
15
+ SpecsSequence,
16
+ )
17
+
18
+
19
+ class SpecsDetector(
20
+ ElectronAnalyserDetector[
21
+ SpecsSequence[TLensMode, TPsuMode],
22
+ SpecsAnalyserDriverIO[TLensMode, TPsuMode],
23
+ SpecsRegion[TLensMode, TPsuMode],
24
+ ],
25
+ Generic[TLensMode, TPsuMode],
26
+ ):
27
+ def __init__(
28
+ self,
29
+ prefix: str,
30
+ lens_mode_type: type[TLensMode],
31
+ psu_mode_type: type[TPsuMode],
32
+ energy_source: DualEnergySource | EnergySource,
33
+ name: str = "",
34
+ ):
35
+ # Save to class so takes part with connect()
36
+ self.driver = SpecsAnalyserDriverIO[TLensMode, TPsuMode](
37
+ prefix, lens_mode_type, psu_mode_type
38
+ )
39
+
40
+ controller = ElectronAnalyserController[
41
+ SpecsAnalyserDriverIO[TLensMode, TPsuMode], SpecsRegion[TLensMode, TPsuMode]
42
+ ](self.driver, energy_source, 0)
43
+
44
+ sequence_class = SpecsSequence[lens_mode_type, psu_mode_type]
45
+
46
+ super().__init__(sequence_class, controller, name)
@@ -4,22 +4,19 @@ from typing import Generic
4
4
  import numpy as np
5
5
  from ophyd_async.core import (
6
6
  Array1D,
7
+ AsyncStatus,
7
8
  SignalR,
8
9
  StandardReadableFormat,
9
10
  derived_signal_r,
10
11
  )
11
12
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
12
13
 
13
- from dodal.devices.electron_analyser.abstract.base_driver_io import (
14
+ from dodal.devices.electron_analyser.base.base_driver_io import (
14
15
  AbstractAnalyserDriverIO,
15
16
  )
16
- from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
17
- from dodal.devices.electron_analyser.energy_sources import (
18
- DualEnergySource,
19
- EnergySource,
20
- )
21
- from dodal.devices.electron_analyser.specs.enums import AcquisitionMode
22
- from dodal.devices.electron_analyser.specs.region import SpecsRegion
17
+ from dodal.devices.electron_analyser.base.base_region import TLensMode, TPsuMode
18
+ from dodal.devices.electron_analyser.specs.specs_enums import AcquisitionMode
19
+ from dodal.devices.electron_analyser.specs.specs_region import SpecsRegion
23
20
 
24
21
 
25
22
  class SpecsAnalyserDriverIO(
@@ -37,7 +34,6 @@ class SpecsAnalyserDriverIO(
37
34
  prefix: str,
38
35
  lens_mode_type: type[TLensMode],
39
36
  psu_mode_type: type[TPsuMode],
40
- energy_source: EnergySource | DualEnergySource,
41
37
  name: str = "",
42
38
  ) -> None:
43
39
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
@@ -59,29 +55,30 @@ class SpecsAnalyserDriverIO(
59
55
  lens_mode_type=lens_mode_type,
60
56
  psu_mode_type=psu_mode_type,
61
57
  pass_energy_type=float,
62
- energy_source=energy_source,
63
58
  name=name,
64
59
  )
65
60
 
66
- async def _set_region(self, ke_region: SpecsRegion[TLensMode, TPsuMode]):
61
+ @AsyncStatus.wrap
62
+ async def set(self, epics_region: SpecsRegion[TLensMode, TPsuMode]):
67
63
  await asyncio.gather(
68
- self.region_name.set(ke_region.name),
69
- self.low_energy.set(ke_region.low_energy),
70
- self.high_energy.set(ke_region.high_energy),
71
- self.slices.set(ke_region.slices),
72
- self.acquire_time.set(ke_region.acquire_time),
73
- self.lens_mode.set(ke_region.lens_mode),
74
- self.pass_energy.set(ke_region.pass_energy),
75
- self.iterations.set(ke_region.iterations),
76
- self.acquisition_mode.set(ke_region.acquisition_mode),
77
- self.snapshot_values.set(ke_region.values),
78
- self.psu_mode.set(ke_region.psu_mode),
64
+ self.region_name.set(epics_region.name),
65
+ self.low_energy.set(epics_region.low_energy),
66
+ self.high_energy.set(epics_region.high_energy),
67
+ self.slices.set(epics_region.slices),
68
+ self.acquire_time.set(epics_region.acquire_time),
69
+ self.lens_mode.set(epics_region.lens_mode),
70
+ self.pass_energy.set(epics_region.pass_energy),
71
+ self.iterations.set(epics_region.iterations),
72
+ self.acquisition_mode.set(epics_region.acquisition_mode),
73
+ self.snapshot_values.set(epics_region.values),
74
+ self.psu_mode.set(epics_region.psu_mode),
75
+ self.energy_mode.set(epics_region.energy_mode),
79
76
  )
80
- if ke_region.acquisition_mode == AcquisitionMode.FIXED_TRANSMISSION:
81
- await self.energy_step.set(ke_region.energy_step)
77
+ if epics_region.acquisition_mode == AcquisitionMode.FIXED_TRANSMISSION:
78
+ await self.energy_step.set(epics_region.energy_step)
82
79
 
83
- if ke_region.acquisition_mode == AcquisitionMode.FIXED_ENERGY:
84
- await self.centre_energy.set(ke_region.centre_energy)
80
+ if epics_region.acquisition_mode == AcquisitionMode.FIXED_ENERGY:
81
+ await self.centre_energy.set(epics_region.centre_energy)
85
82
 
86
83
  def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
87
84
  angle_axis = derived_signal_r(
@@ -2,12 +2,13 @@ from typing import Generic
2
2
 
3
3
  from pydantic import Field
4
4
 
5
- from dodal.devices.electron_analyser.abstract.base_region import (
5
+ from dodal.devices.electron_analyser.base.base_region import (
6
6
  AbstractBaseRegion,
7
7
  AbstractBaseSequence,
8
+ TLensMode,
9
+ TPsuMode,
8
10
  )
9
- from dodal.devices.electron_analyser.abstract.types import TLensMode, TPsuMode
10
- from dodal.devices.electron_analyser.specs.enums import AcquisitionMode
11
+ from dodal.devices.electron_analyser.specs.specs_enums import AcquisitionMode
11
12
 
12
13
 
13
14
  class SpecsRegion(
@@ -1,7 +1,7 @@
1
- from .detector import VGScientaDetector
2
- from .driver_io import VGScientaAnalyserDriverIO
3
- from .enums import AcquisitionMode, DetectorMode
4
- from .region import VGScientaRegion, VGScientaSequence
1
+ from .vgscienta_detector import VGScientaDetector
2
+ from .vgscienta_driver_io import VGScientaAnalyserDriverIO
3
+ from .vgscienta_enums import AcquisitionMode, DetectorMode
4
+ from .vgscienta_region import VGScientaRegion, VGScientaSequence
5
5
 
6
6
  __all__ = [
7
7
  "VGScientaDetector",