dls-dodal 1.46.0__py3-none-any.whl → 1.47.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 (67) hide show
  1. {dls_dodal-1.46.0.dist-info → dls_dodal-1.47.0.dist-info}/METADATA +1 -1
  2. {dls_dodal-1.46.0.dist-info → dls_dodal-1.47.0.dist-info}/RECORD +62 -51
  3. {dls_dodal-1.46.0.dist-info → dls_dodal-1.47.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/__init__.py +0 -1
  6. dodal/beamlines/b07.py +2 -6
  7. dodal/beamlines/b07_1.py +1 -3
  8. dodal/beamlines/i03.py +12 -15
  9. dodal/beamlines/i04.py +48 -16
  10. dodal/beamlines/i09.py +1 -3
  11. dodal/beamlines/i09_1.py +1 -3
  12. dodal/beamlines/i23.py +17 -1
  13. dodal/beamlines/p38.py +1 -1
  14. dodal/beamlines/p60.py +2 -6
  15. dodal/beamlines/p99.py +48 -4
  16. dodal/common/beamlines/beamline_parameters.py +1 -2
  17. dodal/common/data_util.py +4 -0
  18. dodal/devices/aperturescatterguard.py +47 -47
  19. dodal/devices/current_amplifiers/struck_scaler_counter.py +1 -1
  20. dodal/devices/diamond_filter.py +5 -17
  21. dodal/devices/eiger.py +1 -1
  22. dodal/devices/electron_analyser/__init__.py +8 -0
  23. dodal/devices/electron_analyser/abstract/__init__.py +28 -0
  24. dodal/devices/electron_analyser/abstract/base_detector.py +210 -0
  25. dodal/devices/electron_analyser/abstract/base_driver_io.py +121 -0
  26. dodal/devices/electron_analyser/{abstract_region.py → abstract/base_region.py} +2 -9
  27. dodal/devices/electron_analyser/specs/__init__.py +11 -0
  28. dodal/devices/electron_analyser/specs/detector.py +29 -0
  29. dodal/devices/electron_analyser/specs/driver_io.py +64 -0
  30. dodal/devices/electron_analyser/{specs_region.py → specs/region.py} +1 -1
  31. dodal/devices/electron_analyser/types.py +6 -0
  32. dodal/devices/electron_analyser/util.py +13 -0
  33. dodal/devices/electron_analyser/vgscienta/__init__.py +12 -0
  34. dodal/devices/electron_analyser/vgscienta/detector.py +36 -0
  35. dodal/devices/electron_analyser/vgscienta/driver_io.py +39 -0
  36. dodal/devices/electron_analyser/{vgscienta_region.py → vgscienta/region.py} +1 -1
  37. dodal/devices/fast_grid_scan.py +7 -9
  38. dodal/devices/i03/__init__.py +3 -0
  39. dodal/devices/i04/__init__.py +3 -0
  40. dodal/devices/i04/constants.py +9 -0
  41. dodal/devices/i04/murko_results.py +195 -0
  42. dodal/devices/i10/diagnostics.py +9 -61
  43. dodal/devices/i24/focus_mirrors.py +9 -13
  44. dodal/devices/i24/pilatus_metadata.py +9 -9
  45. dodal/devices/i24/pmac.py +19 -14
  46. dodal/devices/{i03 → mx_phase1}/beamstop.py +6 -12
  47. dodal/devices/oav/oav_calculations.py +2 -2
  48. dodal/devices/oav/oav_detector.py +32 -22
  49. dodal/devices/oav/utils.py +2 -2
  50. dodal/devices/p99/andor2_point.py +41 -0
  51. dodal/devices/positioner.py +49 -0
  52. dodal/devices/tetramm.py +5 -2
  53. dodal/devices/util/adjuster_plans.py +1 -1
  54. dodal/devices/zebra/zebra_constants_mapping.py +1 -1
  55. dodal/devices/zocalo/__init__.py +0 -3
  56. dodal/devices/zocalo/zocalo_results.py +6 -32
  57. dodal/log.py +14 -14
  58. dodal/plan_stubs/electron_analyser/__init__.py +3 -0
  59. dodal/plan_stubs/electron_analyser/{configure_controller.py → configure_driver.py} +30 -18
  60. dodal/common/signal_utils.py +0 -88
  61. dodal/devices/electron_analyser/abstract_analyser_io.py +0 -47
  62. dodal/devices/electron_analyser/specs_analyser_io.py +0 -19
  63. dodal/devices/electron_analyser/vgscienta_analyser_io.py +0 -26
  64. dodal/devices/logging_ophyd_device.py +0 -17
  65. {dls_dodal-1.46.0.dist-info → dls_dodal-1.47.0.dist-info}/entry_points.txt +0 -0
  66. {dls_dodal-1.46.0.dist-info → dls_dodal-1.47.0.dist-info}/licenses/LICENSE +0 -0
  67. {dls_dodal-1.46.0.dist-info → dls_dodal-1.47.0.dist-info}/top_level.txt +0 -0
dodal/beamlines/p99.py CHANGED
@@ -1,7 +1,22 @@
1
- from dodal.common.beamlines.beamline_utils import device_factory, set_beamline
1
+ from pathlib import Path
2
+
3
+ from ophyd_async.epics.adandor import Andor2Detector
4
+
5
+ from dodal.common.beamlines.beamline_utils import (
6
+ device_factory,
7
+ get_path_provider,
8
+ set_beamline,
9
+ set_path_provider,
10
+ )
11
+ from dodal.common.beamlines.device_helpers import CAM_SUFFIX, HDF5_SUFFIX
12
+ from dodal.common.visit import (
13
+ LocalDirectoryServiceClient,
14
+ StaticVisitPathProvider,
15
+ )
2
16
  from dodal.devices.attenuator.filter import FilterMotor
3
17
  from dodal.devices.attenuator.filter_selections import P99FilterSelections
4
18
  from dodal.devices.motors import XYZPositioner
19
+ from dodal.devices.p99.andor2_point import Andor2Point
5
20
  from dodal.devices.p99.sample_stage import SampleAngleStage
6
21
  from dodal.log import set_beamline as set_log_beamline
7
22
  from dodal.utils import BeamlinePrefix, get_beamline_name
@@ -19,9 +34,7 @@ def angle_stage() -> SampleAngleStage:
19
34
 
20
35
  @device_factory()
21
36
  def filter() -> FilterMotor:
22
- return FilterMotor(
23
- f"{PREFIX.beamline_prefix}-MO-STAGE-02:MP:SELECT", P99FilterSelections
24
- )
37
+ return FilterMotor(f"{PREFIX.beamline_prefix}-MO-STAGE-02:MP:", P99FilterSelections)
25
38
 
26
39
 
27
40
  @device_factory()
@@ -32,3 +45,34 @@ def sample_stage() -> XYZPositioner:
32
45
  @device_factory()
33
46
  def lab_stage() -> XYZPositioner:
34
47
  return XYZPositioner(f"{PREFIX.beamline_prefix}-MO-STAGE-02:LAB:")
48
+
49
+
50
+ set_path_provider(
51
+ StaticVisitPathProvider(
52
+ BL,
53
+ Path("/dls/p99/data/2024/cm37284-2/processing/writenData"),
54
+ client=LocalDirectoryServiceClient(), # RemoteDirectoryServiceClient("http://p99-control:8088/api"),
55
+ )
56
+ )
57
+
58
+
59
+ @device_factory()
60
+ def andor2_det() -> Andor2Detector:
61
+ """Andor model:DU897_BV."""
62
+ return Andor2Detector(
63
+ prefix=f"{PREFIX.beamline_prefix}-EA-DET-03:",
64
+ path_provider=get_path_provider(),
65
+ drv_suffix=CAM_SUFFIX,
66
+ fileio_suffix=HDF5_SUFFIX,
67
+ )
68
+
69
+
70
+ @device_factory()
71
+ def andor2_point() -> Andor2Point:
72
+ """Using the andor2 as if it is a massive point detector, read the meanValue and total after
73
+ a picture is taken."""
74
+ return Andor2Point(
75
+ prefix=f"{PREFIX.beamline_prefix}-EA-DET-03:",
76
+ drv_suffix=CAM_SUFFIX,
77
+ read_uncached={"mean": "STAT:MeanValue_RBV", "total": "STAT:Total_RBV"},
78
+ )
@@ -8,7 +8,6 @@ BEAMLINE_PARAMETER_KEYWORDS = ["FB", "FULL", "deadtime"]
8
8
  BEAMLINE_PARAMETER_PATHS = {
9
9
  "i03": "/dls_sw/i03/software/daq_configuration/domain/beamlineParameters",
10
10
  "i04": "/dls_sw/i04/software/gda_versions/gda_9_34/workspace_git/gda-mx.git/configurations/i04-config/scripts/beamlineParameters",
11
- "s03": "tests/test_data/test_beamline_parameters.txt",
12
11
  }
13
12
 
14
13
 
@@ -92,7 +91,7 @@ def get_beamline_parameters(beamline_param_path: str | None = None):
92
91
  """Loads the beamline parameters from the specified path, or according to the
93
92
  environment variable if none is given"""
94
93
  if not beamline_param_path:
95
- beamline_name = get_beamline_name("s03")
94
+ beamline_name = get_beamline_name("i03")
96
95
  beamline_param_path = BEAMLINE_PARAMETER_PATHS.get(beamline_name)
97
96
  if beamline_param_path is None:
98
97
  raise KeyError(
dodal/common/data_util.py CHANGED
@@ -1,3 +1,4 @@
1
+ import os
1
2
  from typing import TypeVar
2
3
 
3
4
  from pydantic import BaseModel
@@ -9,6 +10,9 @@ def load_json_file_to_class(
9
10
  t: type[TBaseModel],
10
11
  file: str,
11
12
  ) -> TBaseModel:
13
+ if not os.path.isfile(file):
14
+ raise FileNotFoundError(f"Cannot find file {file}")
15
+
12
16
  with open(file) as f:
13
17
  json_obj = f.read()
14
18
  cls = t.model_validate_json(json_obj)
@@ -2,17 +2,18 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
 
5
- from bluesky.protocols import Movable, Preparable
5
+ from bluesky.protocols import Preparable
6
6
  from ophyd_async.core import (
7
7
  AsyncStatus,
8
8
  StandardReadable,
9
9
  StandardReadableFormat,
10
10
  StrictEnum,
11
+ derived_signal_r,
12
+ derived_signal_rw,
11
13
  )
12
14
  from pydantic import BaseModel, Field
13
15
 
14
16
  from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
15
- from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
16
17
  from dodal.devices.aperture import Aperture
17
18
  from dodal.devices.scatterguard import Scatterguard
18
19
 
@@ -123,21 +124,21 @@ def load_positions_from_beamline_parameters(
123
124
  }
124
125
 
125
126
 
126
- class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable):
127
+ class ApertureScatterguard(StandardReadable, Preparable):
127
128
  """Move the aperture and scatterguard assembly in a safe way. There are two ways to
128
129
  interact with the device depending on if you want simplicity or move flexibility.
129
130
 
130
131
  Examples:
131
132
  The simple interface is using::
132
133
 
133
- await aperture_scatterguard.set(ApertureValue.LARGE)
134
+ await aperture_scatterguard.selected_aperture.set(ApertureValue.LARGE)
134
135
 
135
136
  This will move the assembly so that the large aperture is in the beam, regardless
136
137
  of where the assembly currently is.
137
138
 
138
139
  We may also want to move the assembly out of the beam with::
139
140
 
140
- await aperture_scatterguard.set(ApertureValue.OUT_OF_BEAM)
141
+ await aperture_scatterguard.selected_aperture.set(ApertureValue.OUT_OF_BEAM)
141
142
 
142
143
  Note, to make sure we do this as quickly as possible, the scatterguard will stay
143
144
  in the same position relative to the aperture.
@@ -149,7 +150,7 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
149
150
 
150
151
  Then, at a later time, move back into the beam::
151
152
 
152
- await aperture_scatterguard.set(ApertureValue.LARGE)
153
+ await aperture_scatterguard.selected_aperture.set(ApertureValue.LARGE)
153
154
 
154
155
  Given the prepare has been done this move will now be faster as only the y is
155
156
  left to move.
@@ -164,11 +165,24 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
164
165
  ) -> None:
165
166
  self.aperture = Aperture(prefix + "-MO-MAPT-01:")
166
167
  self.scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
167
- self.radius = create_r_hardware_backed_soft_signal(
168
- float, self._get_current_radius, units="µm"
169
- )
170
168
  self._loaded_positions = loaded_positions
171
169
  self._tolerances = tolerances
170
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
171
+ self.selected_aperture = derived_signal_rw(
172
+ self._get_current_aperture_position,
173
+ self._set_current_aperture_position,
174
+ large=self.aperture.large,
175
+ medium=self.aperture.medium,
176
+ small=self.aperture.small,
177
+ current_ap_y=self.aperture.y.user_readback,
178
+ )
179
+
180
+ self.radius = derived_signal_r(
181
+ self._get_current_radius,
182
+ current_aperture=self.selected_aperture,
183
+ derived_units="µm",
184
+ )
185
+
172
186
  self.add_readables(
173
187
  [
174
188
  self.aperture.x.user_readback,
@@ -180,17 +194,9 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
180
194
  ],
181
195
  )
182
196
 
183
- with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
184
- self.selected_aperture = create_r_hardware_backed_soft_signal(
185
- ApertureValue, self._get_current_aperture_position
186
- )
187
-
188
197
  super().__init__(name)
189
198
 
190
- @AsyncStatus.wrap
191
- async def set(self, value: ApertureValue):
192
- """This set will move the aperture into the beam or move the whole assembly out"""
193
-
199
+ async def _set_current_aperture_position(self, value: ApertureValue) -> None:
194
200
  position = self._loaded_positions[value]
195
201
  await self._check_safe_to_move(position.aperture_z)
196
202
 
@@ -231,6 +237,27 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
231
237
  "triggering another move."
232
238
  )
233
239
 
240
+ def _get_current_radius(self, current_aperture: ApertureValue) -> float:
241
+ return self._loaded_positions[current_aperture].radius
242
+
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
246
+
247
+ def _get_current_aperture_position(
248
+ self, large: float, medium: float, small: float, current_ap_y: float
249
+ ) -> ApertureValue:
250
+ if large == 1:
251
+ return ApertureValue.LARGE
252
+ elif medium == 1:
253
+ return ApertureValue.MEDIUM
254
+ elif small == 1:
255
+ return ApertureValue.SMALL
256
+ elif self._is_out_of_beam(current_ap_y):
257
+ return ApertureValue.OUT_OF_BEAM
258
+
259
+ raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
260
+
234
261
  async def _safe_move_whilst_in_beam(self, position: AperturePosition):
235
262
  """
236
263
  Move the aperture and scatterguard combo safely to a new position.
@@ -282,33 +309,6 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
282
309
  self.scatterguard.y.set(scatterguard_y),
283
310
  )
284
311
 
285
- async def _is_out_of_beam(self) -> bool:
286
- current_ap_y = await self.aperture.y.user_readback.get_value()
287
- out_ap_y = self._loaded_positions[ApertureValue.OUT_OF_BEAM].aperture_y
288
- return current_ap_y <= out_ap_y + self._tolerances.aperture_y
289
-
290
- async def _get_current_aperture_position(self) -> ApertureValue:
291
- """
292
- Returns the current aperture position using readback values
293
- for SMALL, MEDIUM, LARGE. ROBOT_LOAD position defined when
294
- mini aperture y <= ROBOT_LOAD.location.aperture_y + tolerance.
295
- If no position is found then raises InvalidApertureMove.
296
- """
297
- if await self.aperture.large.get_value(cached=False) == 1:
298
- return ApertureValue.LARGE
299
- elif await self.aperture.medium.get_value(cached=False) == 1:
300
- return ApertureValue.MEDIUM
301
- elif await self.aperture.small.get_value(cached=False) == 1:
302
- return ApertureValue.SMALL
303
- elif await self._is_out_of_beam():
304
- return ApertureValue.OUT_OF_BEAM
305
-
306
- raise InvalidApertureMove("Current aperture/scatterguard state unrecognised")
307
-
308
- async def _get_current_radius(self) -> float:
309
- current_value = await self._get_current_aperture_position()
310
- return self._loaded_positions[current_value].radius
311
-
312
312
  @AsyncStatus.wrap
313
313
  async def prepare(self, value: ApertureValue):
314
314
  """Moves the assembly to the position for the specified aperture, whilst keeping
@@ -317,7 +317,7 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
317
317
  Moving the assembly whilst out of the beam has no collision risk so we can just
318
318
  move all the motors together.
319
319
  """
320
- if await self._is_out_of_beam():
320
+ if self._is_out_of_beam(await self.aperture.y.user_readback.get_value()):
321
321
  aperture_x, _, aperture_z, scatterguard_x, scatterguard_y = (
322
322
  self._loaded_positions[value].values
323
323
  )
@@ -329,4 +329,4 @@ class ApertureScatterguard(StandardReadable, Movable[ApertureValue], Preparable)
329
329
  self.scatterguard.y.set(scatterguard_y),
330
330
  )
331
331
  else:
332
- await self.set(value)
332
+ await self.selected_aperture.set(value)
@@ -18,7 +18,7 @@ class CountMode(StrictEnum):
18
18
 
19
19
  class CountState(StrictEnum):
20
20
  DONE = "Done"
21
- COUNT = "Count" # type: ignore
21
+ COUNT = "Count"
22
22
 
23
23
 
24
24
  COUNT_PER_VOLTAGE = 100000
@@ -1,8 +1,8 @@
1
- from typing import Generic, TypeVar
1
+ from typing import TypeVar
2
2
 
3
- from ophyd_async.core import StandardReadable, StrictEnum
4
- from ophyd_async.epics.core import epics_signal_rw
5
- from ophyd_async.epics.motor import Motor
3
+ from ophyd_async.core import StrictEnum
4
+
5
+ from dodal.devices.positioner import Positioner1D
6
6
 
7
7
 
8
8
  class _Filters(StrictEnum):
@@ -25,22 +25,10 @@ class I04Filters(_Filters):
25
25
  T = TypeVar("T", bound=_Filters)
26
26
 
27
27
 
28
- class DiamondFilter(StandardReadable, Generic[T]):
28
+ class DiamondFilter(Positioner1D[T]):
29
29
  """
30
30
  A filter set that is used to reduce the heat load on the monochromator.
31
31
 
32
32
  It has 4 slots that can contain filters of different thickness. Changing the thickness
33
33
  signal will move the filter set to select this filter.
34
34
  """
35
-
36
- def __init__(
37
- self,
38
- prefix: str,
39
- data_type: type[T],
40
- name: str = "",
41
- ) -> None:
42
- with self.add_children_as_readables():
43
- self.y_motor = Motor(prefix + "Y")
44
- self.thickness = epics_signal_rw(data_type, f"{prefix}Y:MP:SELECT")
45
-
46
- super().__init__(name)
dodal/devices/eiger.py CHANGED
@@ -35,7 +35,7 @@ class InternalEigerTriggerMode(Enum):
35
35
  AVAILABLE_TIMEOUTS = {
36
36
  "i03": EigerTimeouts(
37
37
  stale_params_timeout=60,
38
- general_status_timeout=10,
38
+ general_status_timeout=20,
39
39
  meta_file_ready_timeout=30,
40
40
  all_frames_timeout=120, # Long timeout for meta file to compensate for filesystem issues
41
41
  arming_timeout=60,
@@ -0,0 +1,8 @@
1
+ from .types import EnergyMode
2
+ from .util import to_binding_energy, to_kinetic_energy
3
+
4
+ __all__ = [
5
+ "to_binding_energy",
6
+ "to_kinetic_energy",
7
+ "EnergyMode",
8
+ ]
@@ -0,0 +1,28 @@
1
+ from .base_detector import (
2
+ AbstractAnalyserDriverIO,
3
+ AbstractElectronAnalyserDetector,
4
+ AbstractElectronAnalyserRegionDetector,
5
+ TAbstractElectronAnalyserDetector,
6
+ TAbstractElectronAnalyserRegionDetector,
7
+ )
8
+ from .base_driver_io import AbstractAnalyserDriverIO, TAbstractAnalyserDriverIO
9
+ from .base_region import (
10
+ AbstractBaseRegion,
11
+ AbstractBaseSequence,
12
+ TAbstractBaseRegion,
13
+ TAbstractBaseSequence,
14
+ )
15
+
16
+ __all__ = [
17
+ "AbstractBaseRegion",
18
+ "AbstractBaseSequence",
19
+ "TAbstractBaseRegion",
20
+ "TAbstractBaseSequence",
21
+ "AbstractAnalyserDriverIO",
22
+ "AbstractElectronAnalyserDetector",
23
+ "AbstractElectronAnalyserRegionDetector",
24
+ "TAbstractElectronAnalyserDetector",
25
+ "TAbstractElectronAnalyserRegionDetector",
26
+ "AbstractAnalyserDriverIO",
27
+ "TAbstractAnalyserDriverIO",
28
+ ]
@@ -0,0 +1,210 @@
1
+ import asyncio
2
+ from abc import abstractmethod
3
+ from typing import Generic, TypeVar
4
+
5
+ from bluesky.protocols import (
6
+ Reading,
7
+ Stageable,
8
+ Triggerable,
9
+ )
10
+ from event_model import DataKey
11
+ from ophyd_async.core import (
12
+ AsyncStatus,
13
+ Device,
14
+ Reference,
15
+ )
16
+ from ophyd_async.core._protocol import AsyncConfigurable, AsyncReadable
17
+ from ophyd_async.epics.adcore import (
18
+ ADBaseController,
19
+ )
20
+
21
+ from dodal.common.data_util import load_json_file_to_class
22
+ from dodal.devices.electron_analyser.abstract.base_driver_io import (
23
+ AbstractAnalyserDriverIO,
24
+ TAbstractAnalyserDriverIO,
25
+ )
26
+ from dodal.devices.electron_analyser.abstract.base_region import (
27
+ TAbstractBaseRegion,
28
+ TAbstractBaseSequence,
29
+ )
30
+
31
+
32
+ class AnalyserController(ADBaseController[AbstractAnalyserDriverIO]):
33
+ def get_deadtime(self, exposure: float | None) -> float:
34
+ return 0
35
+
36
+
37
+ class BaseElectronAnalyserDetector(
38
+ Device,
39
+ Stageable,
40
+ Triggerable,
41
+ AsyncReadable,
42
+ AsyncConfigurable,
43
+ Generic[TAbstractAnalyserDriverIO],
44
+ ):
45
+ """
46
+ Detector for data acquisition of electron analyser. Can only acquire using settings
47
+ already configured for the device.
48
+
49
+ If possible, this should be changed to inheirt from a StandardDetector. Currently,
50
+ StandardDetector forces you to use a file writer which doesn't apply here.
51
+ See issue https://github.com/bluesky/ophyd-async/issues/888
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ name: str,
57
+ driver: TAbstractAnalyserDriverIO,
58
+ ):
59
+ self.controller: AnalyserController = AnalyserController(driver=driver)
60
+ super().__init__(name)
61
+
62
+ @AsyncStatus.wrap
63
+ async def trigger(self) -> None:
64
+ await self.controller.arm()
65
+ await self.controller.wait_for_idle()
66
+
67
+ @AsyncStatus.wrap
68
+ async def stage(self) -> None:
69
+ """Make sure the detector is idle and ready to be used."""
70
+ await asyncio.gather(self.controller.disarm())
71
+
72
+ @AsyncStatus.wrap
73
+ async def unstage(self) -> None:
74
+ """Disarm the detector."""
75
+ await asyncio.gather(self.controller.disarm())
76
+
77
+ async def read(self) -> dict[str, Reading]:
78
+ return await self.driver.read()
79
+
80
+ async def describe(self) -> dict[str, DataKey]:
81
+ data = await self.driver.describe()
82
+ # Correct the shape for image
83
+ prefix = self.driver.name + "-"
84
+ energy_size = len(await self.driver.energy_axis.get_value())
85
+ angle_size = len(await self.driver.angle_axis.get_value())
86
+ data[prefix + "image"]["shape"] = [angle_size, energy_size]
87
+ return data
88
+
89
+ async def read_configuration(self) -> dict[str, Reading]:
90
+ return await self.driver.read_configuration()
91
+
92
+ async def describe_configuration(self) -> dict[str, DataKey]:
93
+ return await self.driver.describe_configuration()
94
+
95
+ @property
96
+ @abstractmethod
97
+ def driver(self) -> TAbstractAnalyserDriverIO:
98
+ """
99
+ Define property for the driver. Some implementations will store this as a
100
+ reference so it doesn't run into errors with conflicting parents.
101
+ """
102
+
103
+
104
+ class AbstractElectronAnalyserRegionDetector(
105
+ BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO],
106
+ Stageable,
107
+ Generic[TAbstractAnalyserDriverIO, TAbstractBaseRegion],
108
+ ):
109
+ """
110
+ Extends electron analyser detector to configure specific region settings before data
111
+ acqusition. This object must be passed in a driver and store it as a reference. It
112
+ is designed to only exist inside a plan.
113
+ """
114
+
115
+ def __init__(
116
+ self, name: str, driver: TAbstractAnalyserDriverIO, region: TAbstractBaseRegion
117
+ ):
118
+ self._driver_ref = Reference(driver)
119
+ self.region = region
120
+ super().__init__(name, driver)
121
+
122
+ @property
123
+ def driver(self) -> TAbstractAnalyserDriverIO:
124
+ # Store as a reference, this implementation will be given a driver so needs to
125
+ # make sure we don't get conflicting parents.
126
+ return self._driver_ref()
127
+
128
+ @AsyncStatus.wrap
129
+ async def stage(self) -> None:
130
+ super().stage()
131
+ self.configure_region()
132
+
133
+ @abstractmethod
134
+ def configure_region(self):
135
+ """
136
+ Setup analyser with configured region.
137
+ """
138
+
139
+
140
+ TAbstractElectronAnalyserRegionDetector = TypeVar(
141
+ "TAbstractElectronAnalyserRegionDetector",
142
+ bound=AbstractElectronAnalyserRegionDetector,
143
+ )
144
+
145
+
146
+ class AbstractElectronAnalyserDetector(
147
+ BaseElectronAnalyserDetector[TAbstractAnalyserDriverIO],
148
+ Generic[TAbstractAnalyserDriverIO, TAbstractBaseSequence, TAbstractBaseRegion],
149
+ ):
150
+ """
151
+ Electron analyser detector with the additional functionality to load a sequence file
152
+ and create a list of temporary ElectronAnalyserRegionDetector objects. These will
153
+ setup configured region settings before data acquisition.
154
+ """
155
+
156
+ def __init__(
157
+ self, prefix: str, name: str, sequence_class: type[TAbstractBaseSequence]
158
+ ):
159
+ self._driver = self._create_driver(prefix)
160
+ self._sequence_class = sequence_class
161
+ super().__init__(name, self.driver)
162
+
163
+ @property
164
+ def driver(self) -> TAbstractAnalyserDriverIO:
165
+ # This implementation creates the driver and wants this to be the parent so it
166
+ # can be used with connect() method.
167
+ return self._driver
168
+
169
+ def load_sequence(self, filename: str) -> TAbstractBaseSequence:
170
+ return load_json_file_to_class(self._sequence_class, filename)
171
+
172
+ @abstractmethod
173
+ def _create_driver(self, prefix: str) -> TAbstractAnalyserDriverIO:
174
+ """
175
+ Define implementation of the driver used for this detector.
176
+ """
177
+
178
+ @abstractmethod
179
+ def _create_region_detector(
180
+ self, driver: TAbstractAnalyserDriverIO, region: TAbstractBaseRegion
181
+ ) -> AbstractElectronAnalyserRegionDetector[
182
+ TAbstractAnalyserDriverIO, TAbstractBaseRegion
183
+ ]:
184
+ """
185
+ Define a way to create a temporary detector object that will always setup a
186
+ specific region before acquiring.
187
+ """
188
+
189
+ def create_region_detector_list(
190
+ self, filename: str
191
+ ) -> list[
192
+ AbstractElectronAnalyserRegionDetector[
193
+ TAbstractAnalyserDriverIO, TAbstractBaseRegion
194
+ ]
195
+ ]:
196
+ """
197
+ Create a list of detectors that will setup a specific region from the sequence
198
+ file when used.
199
+ """
200
+ seq = self.load_sequence(filename)
201
+ return [
202
+ self._create_region_detector(self.driver, r)
203
+ for r in seq.get_enabled_regions()
204
+ ]
205
+
206
+
207
+ TAbstractElectronAnalyserDetector = TypeVar(
208
+ "TAbstractElectronAnalyserDetector",
209
+ bound=AbstractElectronAnalyserDetector,
210
+ )