dls-dodal 1.50.0__py3-none-any.whl → 1.52.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.50.0.dist-info → dls_dodal-1.52.0.dist-info}/METADATA +5 -5
  2. {dls_dodal-1.50.0.dist-info → dls_dodal-1.52.0.dist-info}/RECORD +76 -68
  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 +13 -2
  7. dodal/beamlines/b07_1.py +13 -2
  8. dodal/beamlines/b16.py +8 -4
  9. dodal/beamlines/b21.py +148 -0
  10. dodal/beamlines/i03.py +10 -12
  11. dodal/beamlines/i04.py +7 -7
  12. dodal/beamlines/i09.py +25 -2
  13. dodal/beamlines/i09_1.py +13 -2
  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 +18 -1
  19. dodal/beamlines/i24.py +5 -5
  20. dodal/beamlines/p45.py +4 -3
  21. dodal/beamlines/p60.py +21 -2
  22. dodal/beamlines/p99.py +19 -5
  23. dodal/beamlines/training_rig.py +3 -3
  24. dodal/common/beamlines/beamline_utils.py +5 -2
  25. dodal/common/device_utils.py +45 -0
  26. dodal/devices/aithre_lasershaping/goniometer.py +4 -5
  27. dodal/devices/aperture.py +4 -7
  28. dodal/devices/aperturescatterguard.py +2 -2
  29. dodal/devices/attenuator/attenuator.py +5 -3
  30. dodal/devices/b07/__init__.py +3 -0
  31. dodal/devices/b07/enums.py +24 -0
  32. dodal/devices/b07_1/__init__.py +3 -0
  33. dodal/devices/b07_1/enums.py +18 -0
  34. dodal/devices/detector/detector_motion.py +19 -17
  35. dodal/devices/electron_analyser/abstract/__init__.py +4 -0
  36. dodal/devices/electron_analyser/abstract/base_driver_io.py +44 -28
  37. dodal/devices/electron_analyser/abstract/base_region.py +20 -7
  38. dodal/devices/electron_analyser/detector.py +3 -13
  39. dodal/devices/electron_analyser/specs/detector.py +24 -4
  40. dodal/devices/electron_analyser/specs/driver_io.py +20 -5
  41. dodal/devices/electron_analyser/specs/region.py +9 -5
  42. dodal/devices/electron_analyser/types.py +21 -5
  43. dodal/devices/electron_analyser/vgscienta/detector.py +22 -7
  44. dodal/devices/electron_analyser/vgscienta/driver_io.py +16 -8
  45. dodal/devices/electron_analyser/vgscienta/region.py +11 -6
  46. dodal/devices/fast_grid_scan.py +1 -2
  47. dodal/devices/i04/constants.py +1 -1
  48. dodal/devices/i09/__init__.py +4 -0
  49. dodal/devices/i09/dcm.py +26 -0
  50. dodal/devices/i09/enums.py +15 -0
  51. dodal/devices/i09_1/__init__.py +3 -0
  52. dodal/devices/i09_1/enums.py +19 -0
  53. dodal/devices/i10/mirrors.py +4 -6
  54. dodal/devices/i10/rasor/rasor_motors.py +0 -14
  55. dodal/devices/i19/beamstop.py +3 -7
  56. dodal/devices/i24/aperture.py +4 -6
  57. dodal/devices/i24/beamstop.py +5 -8
  58. dodal/devices/i24/pmac.py +4 -8
  59. dodal/devices/linkam3.py +25 -81
  60. dodal/devices/motors.py +92 -35
  61. dodal/devices/oav/pin_image_recognition/__init__.py +11 -14
  62. dodal/devices/p45.py +0 -12
  63. dodal/devices/p60/__init__.py +4 -0
  64. dodal/devices/p60/enums.py +10 -0
  65. dodal/devices/p60/lab_xray_source.py +21 -0
  66. dodal/devices/pgm.py +1 -1
  67. dodal/devices/robot.py +11 -7
  68. dodal/devices/smargon.py +8 -9
  69. dodal/devices/tetramm.py +134 -150
  70. dodal/devices/xbpm_feedback.py +6 -3
  71. dodal/devices/zocalo/zocalo_results.py +27 -78
  72. dodal/plans/configure_arm_trigger_and_disarm_detector.py +7 -5
  73. dodal/devices/adsim.py +0 -13
  74. dodal/devices/i18/table.py +0 -14
  75. dodal/devices/i18/thor_labs_stage.py +0 -12
  76. dodal/devices/i24/i24_detector_motion.py +0 -12
  77. dodal/devices/scatterguard.py +0 -11
  78. dodal/devices/training_rig/__init__.py +0 -0
  79. dodal/devices/training_rig/sample_stage.py +0 -10
  80. {dls_dodal-1.50.0.dist-info → dls_dodal-1.52.0.dist-info}/WHEEL +0 -0
  81. {dls_dodal-1.50.0.dist-info → dls_dodal-1.52.0.dist-info}/entry_points.txt +0 -0
  82. {dls_dodal-1.50.0.dist-info → dls_dodal-1.52.0.dist-info}/licenses/LICENSE +0 -0
  83. {dls_dodal-1.50.0.dist-info → dls_dodal-1.52.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,45 @@
1
+ from asyncio import CancelledError, create_task, sleep
2
+ from contextlib import asynccontextmanager
3
+
4
+ from dodal.log import LOGGER
5
+
6
+
7
+ @asynccontextmanager
8
+ async def periodic_reminder(
9
+ message: str = "Waiting",
10
+ schedule: tuple[tuple[int | float, int | None], ...] = ( # seconds, frequency
11
+ (1, 3),
12
+ (5, 3),
13
+ (60, 5),
14
+ (300, 5),
15
+ (1800, 5),
16
+ (3600, None),
17
+ ),
18
+ ):
19
+ """Periodically logs a message according to a schedule with increasing delays.
20
+
21
+ Args:
22
+ message: The message the user wants to output through logging.
23
+ schedule: A tuple list of tuples consisting of (int|float, int|None).
24
+ A sequence of (delay_seconds, count) pairs defining the logging intervals.
25
+ - delay_seconds is the number of seconds to wait between logs.
26
+ - count is how many times to log at this interval. If count is None, it logs indefinitely at that delay.
27
+ """
28
+
29
+ async def _log_loop():
30
+ for delay, count in schedule:
31
+ n = 0
32
+ while count is None or n < count:
33
+ LOGGER.info(message)
34
+ await sleep(delay)
35
+ n += 1
36
+
37
+ task = create_task(_log_loop())
38
+ try:
39
+ yield
40
+ finally:
41
+ task.cancel()
42
+ try:
43
+ await task
44
+ except CancelledError:
45
+ pass
@@ -1,11 +1,13 @@
1
1
  import asyncio
2
2
  import math
3
3
 
4
- from ophyd_async.core import StandardReadable, derived_signal_rw
4
+ from ophyd_async.core import derived_signal_rw
5
5
  from ophyd_async.epics.motor import Motor
6
6
 
7
+ from dodal.devices.motors import XYZStage
7
8
 
8
- class Goniometer(StandardReadable):
9
+
10
+ class Goniometer(XYZStage):
9
11
  """The Aithre lab goniometer and the XYZ stage it sits on.
10
12
 
11
13
  `x`, `y` and `z` control the axes of the positioner at the base, while `sampy` and
@@ -18,9 +20,6 @@ class Goniometer(StandardReadable):
18
20
  """
19
21
 
20
22
  def __init__(self, prefix: str, name: str = "") -> None:
21
- self.x = Motor(prefix + "X")
22
- self.y = Motor(prefix + "Y")
23
- self.z = Motor(prefix + "Z")
24
23
  self.sampy = Motor(prefix + "SAMPY")
25
24
  self.sampz = Motor(prefix + "SAMPZ")
26
25
  self.omega = Motor(prefix + "OMEGA")
dodal/devices/aperture.py CHANGED
@@ -1,14 +1,11 @@
1
- from ophyd_async.core import StandardReadable
2
1
  from ophyd_async.epics.core import epics_signal_r
3
- from ophyd_async.epics.motor import Motor
4
2
 
3
+ from dodal.devices.motors import XYZStage
5
4
 
6
- class Aperture(StandardReadable):
5
+
6
+ class Aperture(XYZStage):
7
7
  def __init__(self, prefix: str, name: str = "") -> None:
8
- self.x = Motor(prefix + "X")
9
- self.y = Motor(prefix + "Y")
10
- self.z = Motor(prefix + "Z")
11
8
  self.small = epics_signal_r(float, prefix + "Y:SMALL_CALC")
12
9
  self.medium = epics_signal_r(float, prefix + "Y:MEDIUM_CALC")
13
10
  self.large = epics_signal_r(float, prefix + "Y:LARGE_CALC")
14
- super().__init__(name)
11
+ super().__init__(prefix, name)
@@ -15,7 +15,7 @@ from pydantic import BaseModel, Field
15
15
 
16
16
  from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
17
17
  from dodal.devices.aperture import Aperture
18
- from dodal.devices.scatterguard import Scatterguard
18
+ from dodal.devices.motors import XYStage
19
19
 
20
20
 
21
21
  class InvalidApertureMove(Exception):
@@ -164,7 +164,7 @@ class ApertureScatterguard(StandardReadable, Preparable):
164
164
  name: str = "",
165
165
  ) -> None:
166
166
  self.aperture = Aperture(prefix + "-MO-MAPT-01:")
167
- self.scatterguard = Scatterguard(prefix + "-MO-SCAT-01:")
167
+ self.scatterguard = XYStage(prefix + "-MO-SCAT-01:")
168
168
  self._loaded_positions = loaded_positions
169
169
  self._tolerances = tolerances
170
170
  with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
@@ -39,10 +39,12 @@ class BinaryFilterAttenuator(ReadOnlyAttenuator, Movable[float]):
39
39
  Where desired_transmission is fraction e.g. 0-1. When the actual_transmission is
40
40
  read from the device it is also fractional"""
41
41
 
42
- def __init__(self, prefix: str, name: str = ""):
42
+ def __init__(self, prefix: str, num_filters: int, name: str = ""):
43
43
  self._calculated_filter_states: DeviceVector[SignalR[int]] = DeviceVector(
44
44
  {
45
- int(digit, 16): epics_signal_r(int, f"{prefix}DEC_TO_BIN.B{digit}")
45
+ int(digit, num_filters): epics_signal_r(
46
+ int, f"{prefix}DEC_TO_BIN.B{digit}"
47
+ )
46
48
  for digit in string.hexdigits
47
49
  if not digit.islower()
48
50
  }
@@ -50,7 +52,7 @@ class BinaryFilterAttenuator(ReadOnlyAttenuator, Movable[float]):
50
52
  self._filters_in_position: DeviceVector[SignalR[bool]] = DeviceVector(
51
53
  {
52
54
  i - 1: epics_signal_r(bool, f"{prefix}FILTER{i}:INLIM")
53
- for i in range(1, 17)
55
+ for i in range(1, num_filters + 1)
54
56
  }
55
57
  )
56
58
 
@@ -0,0 +1,3 @@
1
+ from dodal.devices.b07.enums import Grating, LensMode
2
+
3
+ __all__ = ["Grating", "LensMode"]
@@ -0,0 +1,24 @@
1
+ from ophyd_async.core import StrictEnum, SupersetEnum
2
+
3
+
4
+ class Grating(StrictEnum):
5
+ NI_400 = "400 l/mm Ni"
6
+ NI_1000 = "1000 l/mm Ni"
7
+ PT_600 = "BAD 600 l/mm Pt"
8
+ AU_600 = "600 l/mm Au"
9
+ NO_GRATING = "No Grating"
10
+
11
+
12
+ class LensMode(SupersetEnum):
13
+ LARGE_AREA = "LargeArea"
14
+ HIGH_MAGNIFICATION = "HighMagnification"
15
+ MEDIUM_MAGNIFICATION = "MediumMagnification"
16
+ LOW_MAGNIFICATION = "LowMagnification"
17
+ MEDIUM_ANGULAR_DISPERSION = "MediumAngularDispersion"
18
+ LOW_ANGULAR_DISPERSION = "LowAngularDispersion"
19
+ HIGH_ANGULAR_DISPERSION = "HighAngularDispersion"
20
+ WIDE_ANGLE_MODE = "WideAngleMode"
21
+ MEDIUM_AREA = "MediumArea"
22
+ SMALL_AREA = "SmallArea"
23
+ HIGH_MAGNIFICATION2 = "HighMagnification2"
24
+ NOT_CONNECTED = "Not connected"
@@ -0,0 +1,3 @@
1
+ from dodal.devices.b07_1.enums import Grating, LensMode
2
+
3
+ __all__ = ["Grating", "LensMode"]
@@ -0,0 +1,18 @@
1
+ from ophyd_async.core import StrictEnum, SupersetEnum
2
+
3
+
4
+ class Grating(StrictEnum):
5
+ AU_400 = "400 l/mm Au"
6
+ AU_600 = "600 l/mm Au"
7
+ PT_600 = "600 l/mm Pt"
8
+ AU_1200 = "1200 l/mm Au"
9
+ ML_1200 = "1200 l/mm ML"
10
+ NO_GRATING = "No Grating"
11
+
12
+
13
+ class LensMode(SupersetEnum):
14
+ SMALL_AREA = "SmallArea"
15
+ ANGLE_RESOLVED_MODE_22 = "AngleResolvedMode22"
16
+ ANGLE_RESOLVED_MODE_30 = "AngleResolvedMode30"
17
+ LARGE_AREA = "LargeArea"
18
+ NOT_CONNECTED = "Not connected"
@@ -1,42 +1,44 @@
1
- from ophyd_async.core import Device, StrictEnum
1
+ from ophyd_async.core import StrictEnum
2
2
  from ophyd_async.epics.core import epics_signal_r, 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 ShutterState(StrictEnum):
7
9
  CLOSED = "Closed"
8
10
  OPEN = "Open"
9
11
 
10
12
 
11
- class DetectorMotion(Device):
13
+ class DetectorMotion(XYZStage):
14
+ _device_prefix = "-MO-DET-01:"
15
+ _pmac_prefix = "-MO-PMAC-02:"
16
+
12
17
  def __init__(self, prefix: str, name: str = ""):
13
- device_prefix = "-MO-DET-01:"
14
- pmac_prefix = "-MO-PMAC-02:"
18
+ device_prefix = f"{prefix}{self._device_prefix}"
19
+ pmac_prefix = f"{prefix}{self._pmac_prefix}"
15
20
 
16
- self.upstream_x = Motor(f"{prefix}{device_prefix}UPSTREAMX")
17
- self.downstream_x = Motor(f"{prefix}{device_prefix}DOWNSTREAMX")
18
- self.x = Motor(f"{prefix}{device_prefix}X")
19
- self.y = Motor(f"{prefix}{device_prefix}Y")
20
- self.z = Motor(f"{prefix}{device_prefix}Z")
21
- self.yaw = Motor(f"{prefix}{device_prefix}YAW")
21
+ self.upstream_x = Motor(f"{device_prefix}UPSTREAMX")
22
+ self.downstream_x = Motor(f"{device_prefix}DOWNSTREAMX")
23
+ self.yaw = Motor(f"{device_prefix}YAW")
22
24
 
23
25
  self.shutter = epics_signal_rw(
24
- ShutterState, f"{prefix}{device_prefix}SET_SHUTTER_STATE"
26
+ ShutterState, f"{device_prefix}SET_SHUTTER_STATE"
25
27
  )
26
28
  self.shutter_closed_lim = epics_signal_r(
27
- float, f"{prefix}{device_prefix}CLOSE_LIMIT"
29
+ float, f"{device_prefix}CLOSE_LIMIT"
28
30
  ) # on limit = 1, off = 0
29
31
  self.shutter_open_lim = epics_signal_r(
30
- float, f"{prefix}{device_prefix}OPEN_LIMIT"
32
+ float, f"{device_prefix}OPEN_LIMIT"
31
33
  ) # on limit = 1, off = 0
32
34
  self.z_disabled = epics_signal_r(
33
- float, f"{prefix}{device_prefix}Z:DISABLED"
35
+ float, f"{device_prefix}Z:DISABLED"
34
36
  ) # robot interlock, 0=ok to move, 1=blocked
35
37
  self.crate_power = epics_signal_r(
36
- float, f"{prefix}{pmac_prefix}CRATE2_HEALTHY"
38
+ float, f"{pmac_prefix}CRATE2_HEALTHY"
37
39
  ) # returns 0 if no power
38
40
  self.in_robot_load_safe_position = epics_signal_r(
39
- int, f"{prefix}{pmac_prefix}GPIO_INP_BITS.B2"
41
+ int, f"{pmac_prefix}GPIO_INP_BITS.B2"
40
42
  ) # returns 1 if safe
41
43
 
42
- super().__init__(name)
44
+ super().__init__(device_prefix, name)
@@ -8,6 +8,8 @@ from .base_region import (
8
8
  AbstractBaseSequence,
9
9
  TAbstractBaseRegion,
10
10
  TAbstractBaseSequence,
11
+ TAcquisitionMode,
12
+ TLensMode,
11
13
  )
12
14
 
13
15
  __all__ = [
@@ -15,6 +17,8 @@ __all__ = [
15
17
  "AbstractBaseSequence",
16
18
  "TAbstractBaseRegion",
17
19
  "TAbstractBaseSequence",
20
+ "TAcquisitionMode",
21
+ "TLensMode",
18
22
  "AbstractAnalyserDriverIO",
19
23
  "AbstractElectronAnalyserDetector",
20
24
  "AbstractAnalyserDriverIO",
@@ -1,25 +1,26 @@
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,
10
11
  SignalR,
11
12
  StandardReadable,
12
13
  StandardReadableFormat,
13
- StrictEnum,
14
14
  derived_signal_r,
15
15
  soft_signal_rw,
16
16
  )
17
17
  from ophyd_async.epics.adcore import ADBaseIO
18
18
  from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
19
- from ophyd_async.epics.motor import Motor
20
19
 
21
20
  from dodal.devices.electron_analyser.abstract.base_region import (
22
21
  TAbstractBaseRegion,
22
+ TAcquisitionMode,
23
+ TLensMode,
23
24
  )
24
25
  from dodal.devices.electron_analyser.enums import EnergyMode
25
26
  from dodal.devices.electron_analyser.util import to_binding_energy, to_kinetic_energy
@@ -29,9 +30,8 @@ class AbstractAnalyserDriverIO(
29
30
  ABC,
30
31
  StandardReadable,
31
32
  ADBaseIO,
32
- Preparable,
33
33
  Movable[TAbstractBaseRegion],
34
- Generic[TAbstractBaseRegion],
34
+ Generic[TAbstractBaseRegion, TAcquisitionMode, TLensMode],
35
35
  ):
36
36
  """
37
37
  Generic device to configure electron analyser with new region settings.
@@ -39,8 +39,30 @@ class AbstractAnalyserDriverIO(
39
39
  """
40
40
 
41
41
  def __init__(
42
- self, prefix: str, acquisition_mode_type: type[StrictEnum], name: str = ""
42
+ self,
43
+ prefix: str,
44
+ acquisition_mode_type: type[TAcquisitionMode],
45
+ lens_mode_type: type[TLensMode],
46
+ energy_sources: Mapping[str, SignalR[float]],
47
+ name: str = "",
43
48
  ) -> None:
49
+ """
50
+ Constructor method for setting up electron analyser.
51
+
52
+ Args:
53
+ prefix: Base PV to connect to EPICS for this device.
54
+ acquisition_mode_type: Enum that determines the available acquisition modes
55
+ for this device.
56
+ lens_mode_type: Enum that determines the available lens mode for this
57
+ device.
58
+ energy_sources: Map that pairs a source name to an energy value signal
59
+ (in eV).
60
+ name: Name of the device.
61
+ """
62
+ self.energy_sources = energy_sources
63
+ self.acquisition_mode_type = acquisition_mode_type
64
+ self.lens_mode_type = lens_mode_type
65
+
44
66
  with self.add_children_as_readables():
45
67
  self.image = epics_signal_r(Array1D[np.float64], prefix + "IMAGE")
46
68
  self.spectrum = epics_signal_r(Array1D[np.float64], prefix + "INT_SPECTRUM")
@@ -58,7 +80,7 @@ class AbstractAnalyserDriverIO(
58
80
  self.low_energy = epics_signal_rw(float, prefix + "LOW_ENERGY")
59
81
  self.high_energy = epics_signal_rw(float, prefix + "HIGH_ENERGY")
60
82
  self.slices = epics_signal_rw(int, prefix + "SLICES")
61
- self.lens_mode = epics_signal_rw(str, prefix + "LENS_MODE")
83
+ self.lens_mode = epics_signal_rw(lens_mode_type, prefix + "LENS_MODE")
62
84
  self.pass_energy = epics_signal_rw(
63
85
  self.pass_energy_type, prefix + "PASS_ENERGY"
64
86
  )
@@ -92,26 +114,6 @@ class AbstractAnalyserDriverIO(
92
114
 
93
115
  super().__init__(prefix=prefix, name=name)
94
116
 
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
117
  @AsyncStatus.wrap
116
118
  async def set(self, region: TAbstractBaseRegion):
117
119
  """
@@ -121,10 +123,13 @@ class AbstractAnalyserDriverIO(
121
123
  Args:
122
124
  region: Contains the parameters to setup the driver for a scan.
123
125
  """
126
+
127
+ source = self._get_energy_source(region.excitation_energy_source)
128
+ excitation_energy = await source.get_value() # eV
129
+
124
130
  pass_energy_type = self.pass_energy_type
125
131
  pass_energy = pass_energy_type(region.pass_energy)
126
132
 
127
- excitation_energy = await self.excitation_energy.get_value()
128
133
  low_energy = to_kinetic_energy(
129
134
  region.low_energy, region.energy_mode, excitation_energy
130
135
  )
@@ -141,8 +146,19 @@ class AbstractAnalyserDriverIO(
141
146
  self.pass_energy.set(pass_energy),
142
147
  self.iterations.set(region.iterations),
143
148
  self.acquisition_mode.set(region.acquisition_mode),
149
+ self.excitation_energy.set(excitation_energy),
150
+ self.excitation_energy_source.set(source.name),
144
151
  )
145
152
 
153
+ def _get_energy_source(self, alias_name: str) -> SignalR[float]:
154
+ energy_source = self.energy_sources.get(alias_name)
155
+ if energy_source is None:
156
+ raise KeyError(
157
+ f"'{energy_source}' is an invalid energy source. Avaliable energy "
158
+ + f"sources are '{list(self.energy_sources.keys())}'"
159
+ )
160
+ return energy_source
161
+
146
162
  @abstractmethod
147
163
  def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
148
164
  """
@@ -3,13 +3,11 @@ 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
6
+ from ophyd_async.core import StrictEnum, SupersetEnum
7
7
  from pydantic import BaseModel, Field, model_validator
8
8
 
9
9
  from dodal.devices.electron_analyser.enums import EnergyMode
10
10
 
11
- TStrictEnum = TypeVar("TStrictEnum", bound=StrictEnum)
12
-
13
11
 
14
12
  def java_to_python_case(java_str: str) -> str:
15
13
  """
@@ -46,7 +44,18 @@ def energy_mode_validation(data: dict) -> dict:
46
44
  return data
47
45
 
48
46
 
49
- class AbstractBaseRegion(ABC, JavaToPythonModel, Generic[TStrictEnum]):
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
+ class AbstractBaseRegion(
55
+ ABC,
56
+ JavaToPythonModel,
57
+ Generic[TAcquisitionMode, TLensMode],
58
+ ):
50
59
  """
51
60
  Generic region model that holds the data. Specialised region models should inherit
52
61
  this to extend functionality. All energy units are assumed to be in eV.
@@ -58,9 +67,9 @@ class AbstractBaseRegion(ABC, JavaToPythonModel, Generic[TStrictEnum]):
58
67
  iterations: int = 1
59
68
  excitation_energy_source: str = "source1"
60
69
  # These ones we need subclasses to provide default values
61
- lens_mode: str
70
+ lens_mode: TLensMode
62
71
  pass_energy: int
63
- acquisition_mode: TStrictEnum
72
+ acquisition_mode: TAcquisitionMode
64
73
  low_energy: float
65
74
  high_energy: float
66
75
  step_time: float
@@ -83,7 +92,11 @@ class AbstractBaseRegion(ABC, JavaToPythonModel, Generic[TStrictEnum]):
83
92
  TAbstractBaseRegion = TypeVar("TAbstractBaseRegion", bound=AbstractBaseRegion)
84
93
 
85
94
 
86
- class AbstractBaseSequence(ABC, JavaToPythonModel, Generic[TAbstractBaseRegion]):
95
+ class AbstractBaseSequence(
96
+ ABC,
97
+ JavaToPythonModel,
98
+ Generic[TAbstractBaseRegion, TLensMode],
99
+ ):
87
100
  """
88
101
  Generic sequence model that holds the list of region data. Specialised sequence
89
102
  models should inherit this to extend functionality and define type of region to
@@ -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
+ await 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,9 @@
1
+ from collections.abc import Mapping
2
+ from typing import Generic
3
+
4
+ from ophyd_async.core import SignalR
5
+
6
+ from dodal.devices.electron_analyser.abstract.base_driver_io import TLensMode
1
7
  from dodal.devices.electron_analyser.detector import (
2
8
  ElectronAnalyserDetector,
3
9
  )
@@ -6,8 +12,22 @@ from dodal.devices.electron_analyser.specs.region import SpecsRegion, SpecsSeque
6
12
 
7
13
 
8
14
  class SpecsDetector(
9
- ElectronAnalyserDetector[SpecsAnalyserDriverIO, SpecsSequence, SpecsRegion]
15
+ ElectronAnalyserDetector[
16
+ SpecsAnalyserDriverIO[TLensMode],
17
+ SpecsSequence[TLensMode],
18
+ SpecsRegion[TLensMode],
19
+ ],
20
+ Generic[TLensMode],
10
21
  ):
11
- def __init__(self, prefix: str, name: str = ""):
12
- driver = SpecsAnalyserDriverIO(prefix=prefix)
13
- super().__init__(prefix, SpecsSequence, driver, name)
22
+ def __init__(
23
+ self,
24
+ prefix: str,
25
+ lens_mode_type: type[TLensMode],
26
+ energy_sources: Mapping[str, SignalR[float]],
27
+ name: str = "",
28
+ ):
29
+ driver = SpecsAnalyserDriverIO[TLensMode](
30
+ prefix, lens_mode_type, energy_sources
31
+ )
32
+ seq = SpecsSequence[lens_mode_type]
33
+ super().__init__(seq, driver, name)
@@ -1,4 +1,6 @@
1
1
  import asyncio
2
+ from collections.abc import Mapping
3
+ from typing import Generic
2
4
 
3
5
  import numpy as np
4
6
  from ophyd_async.core import (
@@ -13,12 +15,22 @@ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
13
15
  from dodal.devices.electron_analyser.abstract.base_driver_io import (
14
16
  AbstractAnalyserDriverIO,
15
17
  )
18
+ from dodal.devices.electron_analyser.abstract.base_region import TLensMode
16
19
  from dodal.devices.electron_analyser.specs.enums import AcquisitionMode
17
20
  from dodal.devices.electron_analyser.specs.region import SpecsRegion
18
21
 
19
22
 
20
- class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO[SpecsRegion]):
21
- def __init__(self, prefix: str, name: str = "") -> None:
23
+ class SpecsAnalyserDriverIO(
24
+ AbstractAnalyserDriverIO[SpecsRegion, AcquisitionMode, TLensMode],
25
+ Generic[TLensMode],
26
+ ):
27
+ def __init__(
28
+ self,
29
+ prefix: str,
30
+ lens_mode_type: type[TLensMode],
31
+ energy_sources: Mapping[str, SignalR[float]],
32
+ name: str = "",
33
+ ) -> None:
22
34
  with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
23
35
  # Used for setting up region data acquisition.
24
36
  self.psu_mode = epics_signal_rw(str, prefix + "SCAN_RANGE")
@@ -28,11 +40,14 @@ class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO[SpecsRegion]):
28
40
  # Used to read detector data after acqusition.
29
41
  self.min_angle_axis = epics_signal_r(float, prefix + "Y_MIN_RBV")
30
42
  self.max_angle_axis = epics_signal_r(float, prefix + "Y_MAX_RBV")
43
+ self.energy_channels = epics_signal_r(
44
+ int, prefix + "TOTAL_POINTS_ITERATION_RBV"
45
+ )
31
46
 
32
- super().__init__(prefix, AcquisitionMode, name)
47
+ super().__init__(prefix, AcquisitionMode, lens_mode_type, energy_sources, name)
33
48
 
34
49
  @AsyncStatus.wrap
35
- async def set(self, region: SpecsRegion):
50
+ async def set(self, region: SpecsRegion[TLensMode]):
36
51
  await super().set(region)
37
52
 
38
53
  await asyncio.gather(
@@ -70,7 +85,7 @@ class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO[SpecsRegion]):
70
85
  "eV",
71
86
  min_energy=self.low_energy,
72
87
  max_energy=self.high_energy,
73
- total_points_iterations=self.slices,
88
+ total_points_iterations=self.energy_channels,
74
89
  )
75
90
  return energy_axis
76
91
 
@@ -1,15 +1,18 @@
1
+ from typing import Generic
2
+
1
3
  from pydantic import Field
2
4
 
3
5
  from dodal.devices.electron_analyser.abstract.base_region import (
4
6
  AbstractBaseRegion,
5
7
  AbstractBaseSequence,
8
+ TLensMode,
6
9
  )
7
10
  from dodal.devices.electron_analyser.specs.enums import AcquisitionMode
8
11
 
9
12
 
10
- class SpecsRegion(AbstractBaseRegion[AcquisitionMode]):
13
+ class SpecsRegion(AbstractBaseRegion[AcquisitionMode, TLensMode], Generic[TLensMode]):
11
14
  # Override base class with defaults
12
- lens_mode: str = "SmallArea"
15
+ lens_mode: TLensMode
13
16
  pass_energy: int = 5
14
17
  acquisition_mode: AcquisitionMode = AcquisitionMode.FIXED_TRANSMISSION
15
18
  low_energy: float = Field(default=800, alias="start_energy")
@@ -19,9 +22,10 @@ class SpecsRegion(AbstractBaseRegion[AcquisitionMode]):
19
22
  # Specific to this class
20
23
  values: int = 1
21
24
  centre_energy: float = 0
22
- psu_mode: str = "1.5keV"
25
+ # ToDo - Update to an enum https://github.com/DiamondLightSource/dodal/issues/1328
26
+ psu_mode: str = "1.5kV"
23
27
  estimated_time_in_ms: float = 0
24
28
 
25
29
 
26
- class SpecsSequence(AbstractBaseSequence[SpecsRegion]):
27
- regions: list[SpecsRegion] = Field(default_factory=lambda: [])
30
+ class SpecsSequence(AbstractBaseSequence[SpecsRegion, TLensMode], Generic[TLensMode]):
31
+ regions: list[SpecsRegion[TLensMode]] = Field(default_factory=lambda: [])