dls-dodal 1.45.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 (81) hide show
  1. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/RECORD +76 -64
  3. {dls_dodal-1.45.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 +16 -19
  9. dodal/beamlines/i04.py +49 -17
  10. dodal/beamlines/i09.py +1 -3
  11. dodal/beamlines/i09_1.py +1 -3
  12. dodal/beamlines/i18.py +7 -4
  13. dodal/beamlines/i22.py +3 -3
  14. dodal/beamlines/i23.py +75 -4
  15. dodal/beamlines/p38.py +4 -4
  16. dodal/beamlines/p60.py +2 -6
  17. dodal/beamlines/p99.py +48 -4
  18. dodal/common/beamlines/beamline_parameters.py +1 -2
  19. dodal/common/beamlines/beamline_utils.py +5 -0
  20. dodal/common/data_util.py +4 -0
  21. dodal/devices/aperturescatterguard.py +47 -47
  22. dodal/devices/common_dcm.py +77 -0
  23. dodal/devices/current_amplifiers/struck_scaler_counter.py +1 -1
  24. dodal/devices/diamond_filter.py +5 -17
  25. dodal/devices/eiger.py +1 -1
  26. dodal/devices/electron_analyser/__init__.py +8 -0
  27. dodal/devices/electron_analyser/abstract/__init__.py +28 -0
  28. dodal/devices/electron_analyser/abstract/base_detector.py +210 -0
  29. dodal/devices/electron_analyser/abstract/base_driver_io.py +121 -0
  30. dodal/devices/electron_analyser/{abstract_region.py → abstract/base_region.py} +2 -9
  31. dodal/devices/electron_analyser/specs/__init__.py +11 -0
  32. dodal/devices/electron_analyser/specs/detector.py +29 -0
  33. dodal/devices/electron_analyser/specs/driver_io.py +64 -0
  34. dodal/devices/electron_analyser/{specs_region.py → specs/region.py} +1 -1
  35. dodal/devices/electron_analyser/types.py +6 -0
  36. dodal/devices/electron_analyser/util.py +13 -0
  37. dodal/devices/electron_analyser/vgscienta/__init__.py +12 -0
  38. dodal/devices/electron_analyser/vgscienta/detector.py +36 -0
  39. dodal/devices/electron_analyser/vgscienta/driver_io.py +39 -0
  40. dodal/devices/electron_analyser/{vgscienta_region.py → vgscienta/region.py} +1 -1
  41. dodal/devices/fast_grid_scan.py +7 -9
  42. dodal/devices/i03/__init__.py +3 -0
  43. dodal/devices/{dcm.py → i03/dcm.py} +8 -12
  44. dodal/devices/{undulator_dcm.py → i03/undulator_dcm.py} +6 -4
  45. dodal/devices/i04/__init__.py +3 -0
  46. dodal/devices/i04/constants.py +9 -0
  47. dodal/devices/i04/murko_results.py +195 -0
  48. dodal/devices/i10/diagnostics.py +9 -61
  49. dodal/devices/i13_1/merlin.py +3 -4
  50. dodal/devices/i13_1/merlin_controller.py +1 -1
  51. dodal/devices/i22/dcm.py +10 -12
  52. dodal/devices/i24/dcm.py +8 -17
  53. dodal/devices/i24/focus_mirrors.py +9 -13
  54. dodal/devices/i24/pilatus_metadata.py +9 -9
  55. dodal/devices/i24/pmac.py +19 -14
  56. dodal/devices/{i03 → mx_phase1}/beamstop.py +6 -12
  57. dodal/devices/oav/oav_calculations.py +2 -2
  58. dodal/devices/oav/oav_detector.py +32 -22
  59. dodal/devices/oav/utils.py +2 -2
  60. dodal/devices/p99/andor2_point.py +41 -0
  61. dodal/devices/positioner.py +49 -0
  62. dodal/devices/tetramm.py +8 -6
  63. dodal/devices/turbo_slit.py +2 -2
  64. dodal/devices/util/adjuster_plans.py +1 -1
  65. dodal/devices/zebra/zebra.py +4 -0
  66. dodal/devices/zebra/zebra_constants_mapping.py +1 -1
  67. dodal/devices/zocalo/__init__.py +0 -3
  68. dodal/devices/zocalo/zocalo_results.py +6 -32
  69. dodal/log.py +14 -14
  70. dodal/plan_stubs/data_session.py +10 -1
  71. dodal/plan_stubs/electron_analyser/__init__.py +3 -0
  72. dodal/plan_stubs/electron_analyser/{configure_controller.py → configure_driver.py} +30 -18
  73. dodal/plans/verify_undulator_gap.py +2 -2
  74. dodal/common/signal_utils.py +0 -88
  75. dodal/devices/electron_analyser/abstract_analyser_io.py +0 -47
  76. dodal/devices/electron_analyser/specs_analyser_io.py +0 -19
  77. dodal/devices/electron_analyser/vgscienta_analyser_io.py +0 -26
  78. dodal/devices/logging_ophyd_device.py +0 -17
  79. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/entry_points.txt +0 -0
  80. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/licenses/LICENSE +0 -0
  81. {dls_dodal-1.45.0.dist-info → dls_dodal-1.47.0.dist-info}/top_level.txt +0 -0
@@ -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
+ )
@@ -0,0 +1,121 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import TypeVar
3
+
4
+ import numpy as np
5
+ from ophyd_async.core import (
6
+ Array1D,
7
+ SignalR,
8
+ StandardReadable,
9
+ StandardReadableFormat,
10
+ derived_signal_r,
11
+ soft_signal_rw,
12
+ )
13
+ from ophyd_async.epics.adcore import ADBaseIO
14
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
15
+
16
+ from dodal.devices.electron_analyser.types import EnergyMode
17
+ from dodal.devices.electron_analyser.util import to_binding_energy
18
+
19
+
20
+ class AbstractAnalyserDriverIO(ABC, StandardReadable, ADBaseIO):
21
+ """
22
+ Generic device to configure electron analyser with new region settings.
23
+ Electron analysers should inherit from this class for further specialisation.
24
+ """
25
+
26
+ def __init__(self, prefix: str, name: str = "") -> None:
27
+ with self.add_children_as_readables():
28
+ self.image = epics_signal_r(Array1D[np.float64], prefix + "IMAGE")
29
+ self.spectrum = epics_signal_r(Array1D[np.float64], prefix + "INT_SPECTRUM")
30
+ self.total_intensity = derived_signal_r(
31
+ self._calculate_total_intensity, spectrum=self.spectrum
32
+ )
33
+ self.excitation_energy = soft_signal_rw(float, initial_value=0, units="eV")
34
+
35
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
36
+ # Used for setting up region data acquisition.
37
+ self.region_name = soft_signal_rw(str, initial_value="null")
38
+ self.energy_mode = soft_signal_rw(
39
+ EnergyMode, initial_value=EnergyMode.KINETIC
40
+ )
41
+ self.low_energy = epics_signal_rw(float, prefix + "LOW_ENERGY")
42
+ self.high_energy = epics_signal_rw(float, prefix + "HIGH_ENERGY")
43
+ self.slices = epics_signal_rw(int, prefix + "SLICES")
44
+ self.lens_mode = epics_signal_rw(str, prefix + "LENS_MODE")
45
+ self.pass_energy = epics_signal_rw(
46
+ self.pass_energy_type, prefix + "PASS_ENERGY"
47
+ )
48
+ self.energy_step = epics_signal_rw(float, prefix + "STEP_SIZE")
49
+ self.iterations = epics_signal_rw(int, prefix + "NumExposures")
50
+ self.acquisition_mode = epics_signal_rw(str, prefix + "ACQ_MODE")
51
+
52
+ # Read once per scan after data acquired
53
+ self.energy_axis = self._create_energy_axis_signal(prefix)
54
+ self.binding_energy_axis = derived_signal_r(
55
+ self._calculate_binding_energy_axis,
56
+ "eV",
57
+ energy_axis=self.energy_axis,
58
+ excitation_energy=self.excitation_energy,
59
+ energy_mode=self.energy_mode,
60
+ )
61
+ self.angle_axis = self._create_angle_axis_signal(prefix)
62
+ self.step_time = epics_signal_r(float, prefix + "AcquireTime")
63
+ self.total_steps = epics_signal_r(int, prefix + "TOTAL_POINTS_RBV")
64
+ self.total_time = derived_signal_r(
65
+ self._calculate_total_time,
66
+ "s",
67
+ total_steps=self.total_steps,
68
+ step_time=self.step_time,
69
+ iterations=self.iterations,
70
+ )
71
+
72
+ super().__init__(prefix=prefix, name=name)
73
+
74
+ @abstractmethod
75
+ def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
76
+ """
77
+ The signal that defines the angle axis. Depends on analyser model.
78
+ """
79
+
80
+ @abstractmethod
81
+ def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
82
+ """
83
+ The signal that defines the energy axis. Depends on analyser model.
84
+ """
85
+
86
+ def _calculate_binding_energy_axis(
87
+ self,
88
+ energy_axis: Array1D[np.float64],
89
+ excitation_energy: float,
90
+ energy_mode: EnergyMode,
91
+ ) -> Array1D[np.float64]:
92
+ is_binding = energy_mode == EnergyMode.BINDING
93
+ return np.array(
94
+ [
95
+ to_binding_energy(i_energy_axis, EnergyMode.KINETIC, excitation_energy)
96
+ if is_binding
97
+ else i_energy_axis
98
+ for i_energy_axis in energy_axis
99
+ ]
100
+ )
101
+
102
+ def _calculate_total_time(
103
+ self, total_steps: int, step_time: float, iterations: int
104
+ ) -> float:
105
+ return total_steps * step_time * iterations
106
+
107
+ def _calculate_total_intensity(self, spectrum: Array1D[np.float64]) -> float:
108
+ return float(np.sum(spectrum, dtype=np.float64))
109
+
110
+ @property
111
+ @abstractmethod
112
+ def pass_energy_type(self) -> type:
113
+ """
114
+ Return the type the pass_energy should be. Each one is unfortunately different
115
+ for the underlying analyser software and cannot be changed on epics side.
116
+ """
117
+
118
+
119
+ TAbstractAnalyserDriverIO = TypeVar(
120
+ "TAbstractAnalyserDriverIO", bound=AbstractAnalyserDriverIO
121
+ )
@@ -1,11 +1,12 @@
1
1
  import re
2
2
  from abc import ABC
3
3
  from collections.abc import Callable
4
- from enum import Enum
5
4
  from typing import Generic, TypeVar
6
5
 
7
6
  from pydantic import BaseModel, Field, model_validator
8
7
 
8
+ from dodal.devices.electron_analyser.types import EnergyMode
9
+
9
10
 
10
11
  def java_to_python_case(java_str: str) -> str:
11
12
  """
@@ -42,11 +43,6 @@ def energy_mode_validation(data: dict) -> dict:
42
43
  return data
43
44
 
44
45
 
45
- class EnergyMode(str, Enum):
46
- KINETIC = "Kinetic"
47
- BINDING = "Binding"
48
-
49
-
50
46
  class AbstractBaseRegion(ABC, JavaToPythonModel):
51
47
  """
52
48
  Generic region model that holds the data. Specialised region models should inherit
@@ -73,9 +69,6 @@ class AbstractBaseRegion(ABC, JavaToPythonModel):
73
69
  def is_kinetic_energy(self) -> bool:
74
70
  return self.energy_mode == EnergyMode.KINETIC
75
71
 
76
- def to_kinetic_energy(self, value: float, excitation_energy: float) -> float:
77
- return value if self.is_binding_energy() else excitation_energy - value
78
-
79
72
  @model_validator(mode="before")
80
73
  @classmethod
81
74
  def before_validation(cls, data: dict) -> dict:
@@ -0,0 +1,11 @@
1
+ from .detector import SpecsDetector, SpecsRegionDetector
2
+ from .driver_io import SpecsAnalyserDriverIO
3
+ from .region import SpecsRegion, SpecsSequence
4
+
5
+ __all__ = [
6
+ "SpecsDetector",
7
+ "SpecsRegionDetector",
8
+ "SpecsAnalyserDriverIO",
9
+ "SpecsRegion",
10
+ "SpecsSequence",
11
+ ]
@@ -0,0 +1,29 @@
1
+ from dodal.devices.electron_analyser.abstract.base_detector import (
2
+ AbstractElectronAnalyserDetector,
3
+ AbstractElectronAnalyserRegionDetector,
4
+ )
5
+ from dodal.devices.electron_analyser.specs.driver_io import SpecsAnalyserDriverIO
6
+ from dodal.devices.electron_analyser.specs.region import SpecsRegion, SpecsSequence
7
+
8
+
9
+ class SpecsRegionDetector(
10
+ AbstractElectronAnalyserRegionDetector[SpecsAnalyserDriverIO, SpecsRegion]
11
+ ):
12
+ def configure_region(self):
13
+ # ToDo - Need to move configure plans to here and rewrite tests
14
+ pass
15
+
16
+
17
+ class SpecsDetector(
18
+ AbstractElectronAnalyserDetector[SpecsAnalyserDriverIO, SpecsSequence, SpecsRegion]
19
+ ):
20
+ def __init__(self, prefix: str, name: str):
21
+ super().__init__(prefix, name, SpecsSequence)
22
+
23
+ def _create_driver(self, prefix: str) -> SpecsAnalyserDriverIO:
24
+ return SpecsAnalyserDriverIO(prefix, "driver")
25
+
26
+ def _create_region_detector(
27
+ self, driver: SpecsAnalyserDriverIO, region: SpecsRegion
28
+ ) -> SpecsRegionDetector:
29
+ return SpecsRegionDetector(self.name, driver, region)
@@ -0,0 +1,64 @@
1
+ import numpy as np
2
+ from ophyd_async.core import Array1D, SignalR, StandardReadableFormat, derived_signal_r
3
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
4
+
5
+ from dodal.devices.electron_analyser.abstract.base_driver_io import (
6
+ AbstractAnalyserDriverIO,
7
+ )
8
+
9
+
10
+ class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO):
11
+ def __init__(self, prefix: str, name: str = "") -> None:
12
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
13
+ # Used for setting up region data acquisition.
14
+ self.psu_mode = epics_signal_rw(str, prefix + "SCAN_RANGE")
15
+ self.snapshot_values = epics_signal_rw(int, prefix + "VALUES")
16
+ self.centre_energy = epics_signal_rw(float, prefix + "KINETIC_ENERGY")
17
+
18
+ # Used to read detector data after acqusition.
19
+ self.min_angle_axis = epics_signal_r(float, prefix + "Y_MIN_RBV")
20
+ self.max_angle_axis = epics_signal_r(float, prefix + "Y_MAX_RBV")
21
+
22
+ super().__init__(prefix, name)
23
+
24
+ def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
25
+ angle_axis = derived_signal_r(
26
+ self._calculate_angle_axis,
27
+ min_angle=self.min_angle_axis,
28
+ max_angle=self.max_angle_axis,
29
+ slices=self.slices,
30
+ )
31
+ return angle_axis
32
+
33
+ def _calculate_angle_axis(
34
+ self, min_angle: float, max_angle: float, slices: int
35
+ ) -> Array1D[np.float64]:
36
+ # SPECS returns the extreme edges of the range, not the centre of the pixels
37
+ width = (max_angle - min_angle) / slices
38
+ offset = width / 2
39
+
40
+ axis = np.array([min_angle + offset + i * width for i in range(slices)])
41
+ return axis
42
+
43
+ def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
44
+ energy_axis = derived_signal_r(
45
+ self._calculate_energy_axis,
46
+ "eV",
47
+ min_energy=self.low_energy,
48
+ max_energy=self.high_energy,
49
+ total_points_iterations=self.slices,
50
+ )
51
+ return energy_axis
52
+
53
+ def _calculate_energy_axis(
54
+ self, min_energy: float, max_energy: float, total_points_iterations: int
55
+ ) -> Array1D[np.float64]:
56
+ # Note: Don't use the energy step because of the case where the step doesn't
57
+ # exactly fill the range
58
+ step = (max_energy - min_energy) / (total_points_iterations - 1)
59
+ axis = np.array([min_energy + i * step for i in range(total_points_iterations)])
60
+ return axis
61
+
62
+ @property
63
+ def pass_energy_type(self) -> type:
64
+ return float
@@ -1,6 +1,6 @@
1
1
  from pydantic import Field
2
2
 
3
- from dodal.devices.electron_analyser.abstract_region import (
3
+ from dodal.devices.electron_analyser.abstract.base_region import (
4
4
  AbstractBaseRegion,
5
5
  AbstractBaseSequence,
6
6
  )
@@ -0,0 +1,6 @@
1
+ from ophyd_async.core import StrictEnum
2
+
3
+
4
+ class EnergyMode(StrictEnum):
5
+ KINETIC = "Kinetic"
6
+ BINDING = "Binding"
@@ -0,0 +1,13 @@
1
+ from dodal.devices.electron_analyser.types import EnergyMode
2
+
3
+
4
+ def to_kinetic_energy(
5
+ value: float, value_mode: EnergyMode, excitation_energy: float
6
+ ) -> float:
7
+ return value if value_mode == EnergyMode.KINETIC else excitation_energy - value
8
+
9
+
10
+ def to_binding_energy(
11
+ value: float, value_mode: EnergyMode, excitation_energy: float
12
+ ) -> float:
13
+ return value if value_mode == EnergyMode.BINDING else excitation_energy - value
@@ -0,0 +1,12 @@
1
+ from .detector import VGScientaDetector, VGScientaRegionDetector
2
+ from .driver_io import VGScientaAnalyserDriverIO
3
+ from .region import VGScientaExcitationEnergySource, VGScientaRegion, VGScientaSequence
4
+
5
+ __all__ = [
6
+ "VGScientaDetector",
7
+ "VGScientaRegionDetector",
8
+ "VGScientaAnalyserDriverIO",
9
+ "VGScientaExcitationEnergySource",
10
+ "VGScientaRegion",
11
+ "VGScientaSequence",
12
+ ]
@@ -0,0 +1,36 @@
1
+ from dodal.devices.electron_analyser.abstract.base_detector import (
2
+ AbstractElectronAnalyserDetector,
3
+ AbstractElectronAnalyserRegionDetector,
4
+ )
5
+ from dodal.devices.electron_analyser.vgscienta.driver_io import (
6
+ VGScientaAnalyserDriverIO,
7
+ )
8
+ from dodal.devices.electron_analyser.vgscienta.region import (
9
+ VGScientaRegion,
10
+ VGScientaSequence,
11
+ )
12
+
13
+
14
+ class VGScientaRegionDetector(
15
+ AbstractElectronAnalyserRegionDetector[VGScientaAnalyserDriverIO, VGScientaRegion]
16
+ ):
17
+ def configure_region(self):
18
+ # ToDo - Need to move configure plans to here and rewrite tests
19
+ pass
20
+
21
+
22
+ class VGScientaDetector(
23
+ AbstractElectronAnalyserDetector[
24
+ VGScientaAnalyserDriverIO, VGScientaSequence, VGScientaRegion
25
+ ]
26
+ ):
27
+ def __init__(self, prefix: str, name: str):
28
+ super().__init__(prefix, name, VGScientaSequence)
29
+
30
+ def _create_driver(self, prefix: str) -> VGScientaAnalyserDriverIO:
31
+ return VGScientaAnalyserDriverIO(prefix, "driver")
32
+
33
+ def _create_region_detector(
34
+ self, driver: VGScientaAnalyserDriverIO, region: VGScientaRegion
35
+ ) -> VGScientaRegionDetector:
36
+ return VGScientaRegionDetector(self.name, driver, region)
@@ -0,0 +1,39 @@
1
+ import numpy as np
2
+ from ophyd_async.core import Array1D, SignalR, StandardReadableFormat, soft_signal_rw
3
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
4
+
5
+ from dodal.devices.electron_analyser.abstract.base_driver_io import (
6
+ AbstractAnalyserDriverIO,
7
+ )
8
+ from dodal.devices.electron_analyser.vgscienta.region import (
9
+ DetectorMode,
10
+ )
11
+
12
+
13
+ class VGScientaAnalyserDriverIO(AbstractAnalyserDriverIO):
14
+ def __init__(self, prefix: str, name: str = "") -> None:
15
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
16
+ self.excitation_energy_source = soft_signal_rw(str, initial_value=None)
17
+ # Used for setting up region data acquisition.
18
+ self.centre_energy = epics_signal_rw(float, prefix + "CENTRE_ENERGY")
19
+ self.first_x_channel = epics_signal_rw(int, prefix + "MinX")
20
+ self.first_y_channel = epics_signal_rw(int, prefix + "MinY")
21
+ self.x_channel_size = epics_signal_rw(int, prefix + "SizeX")
22
+ self.y_channel_size = epics_signal_rw(int, prefix + "SizeY")
23
+ self.detector_mode = epics_signal_rw(DetectorMode, prefix + "DETECTOR_MODE")
24
+
25
+ with self.add_children_as_readables():
26
+ # Used to read detector data after acqusition.
27
+ self.external_io = epics_signal_r(Array1D[np.float64], prefix + "EXTIO")
28
+
29
+ super().__init__(prefix, name)
30
+
31
+ def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
32
+ return epics_signal_r(Array1D[np.float64], prefix + "X_SCALE_RBV")
33
+
34
+ def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
35
+ return epics_signal_r(Array1D[np.float64], prefix + "Y_SCALE_RBV")
36
+
37
+ @property
38
+ def pass_energy_type(self) -> type:
39
+ return str
@@ -4,7 +4,7 @@ from enum import Enum
4
4
  from ophyd_async.core import StrictEnum
5
5
  from pydantic import Field
6
6
 
7
- from dodal.devices.electron_analyser.abstract_region import (
7
+ from dodal.devices.electron_analyser.abstract.base_region import (
8
8
  AbstractBaseRegion,
9
9
  AbstractBaseSequence,
10
10
  JavaToPythonModel,
@@ -12,6 +12,7 @@ from ophyd_async.core import (
12
12
  Signal,
13
13
  SignalRW,
14
14
  StandardReadable,
15
+ derived_signal_r,
15
16
  wait_for_value,
16
17
  )
17
18
  from ophyd_async.epics.core import (
@@ -23,7 +24,6 @@ from ophyd_async.epics.core import (
23
24
  from pydantic import field_validator
24
25
  from pydantic.dataclasses import dataclass
25
26
 
26
- from dodal.common.signal_utils import create_r_hardware_backed_soft_signal
27
27
  from dodal.log import LOGGER
28
28
  from dodal.parameters.experiment_parameter_base import AbstractExperimentWithBeamParams
29
29
 
@@ -203,8 +203,11 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
203
203
  self.stop_cmd = epics_signal_x(f"{prefix}STOP.PROC")
204
204
  self.status = epics_signal_r(int, f"{prefix}SCAN_STATUS")
205
205
 
206
- self.expected_images = create_r_hardware_backed_soft_signal(
207
- float, self._calculate_expected_images
206
+ self.expected_images = derived_signal_r(
207
+ self._calculate_expected_images,
208
+ x=self.x_steps,
209
+ y=self.y_steps,
210
+ z=self.z_steps,
208
211
  )
209
212
 
210
213
  self.motion_program = MotionProgram(smargon_prefix)
@@ -231,12 +234,7 @@ class FastGridScanCommon(StandardReadable, Flyable, ABC, Generic[ParamType]):
231
234
  }
232
235
  super().__init__(name)
233
236
 
234
- async def _calculate_expected_images(self):
235
- x, y, z = await asyncio.gather(
236
- self.x_steps.get_value(),
237
- self.y_steps.get_value(),
238
- self.z_steps.get_value(),
239
- )
237
+ def _calculate_expected_images(self, x: float, y: float, z: float) -> float:
240
238
  LOGGER.info(f"Reading num of images found {x, y, z} images in each axis")
241
239
  first_grid = x * y
242
240
  second_grid = x * z
@@ -0,0 +1,3 @@
1
+ from dodal.devices.mx_phase1.beamstop import Beamstop, BeamstopPositions
2
+
3
+ __all__ = ["Beamstop", "BeamstopPositions"]