dls-dodal 1.46.0__py3-none-any.whl → 1.48.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.46.0.dist-info → dls_dodal-1.48.0.dist-info}/METADATA +2 -2
  2. {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/RECORD +74 -63
  3. {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/__init__.py +0 -1
  6. dodal/beamlines/aithre.py +6 -0
  7. dodal/beamlines/b01_1.py +1 -1
  8. dodal/beamlines/b07.py +2 -6
  9. dodal/beamlines/b07_1.py +1 -3
  10. dodal/beamlines/i03.py +33 -21
  11. dodal/beamlines/i04.py +65 -26
  12. dodal/beamlines/i09.py +1 -3
  13. dodal/beamlines/i09_1.py +1 -3
  14. dodal/beamlines/i18.py +1 -1
  15. dodal/beamlines/i19_1.py +9 -6
  16. dodal/beamlines/i23.py +17 -1
  17. dodal/beamlines/i24.py +5 -5
  18. dodal/beamlines/p38.py +1 -1
  19. dodal/beamlines/p60.py +2 -6
  20. dodal/beamlines/p99.py +48 -4
  21. dodal/common/beamlines/beamline_parameters.py +3 -30
  22. dodal/common/data_util.py +4 -0
  23. dodal/devices/aithre_lasershaping/goniometer.py +36 -2
  24. dodal/devices/aithre_lasershaping/laser_robot.py +27 -0
  25. dodal/devices/aperturescatterguard.py +47 -47
  26. dodal/devices/current_amplifiers/struck_scaler_counter.py +1 -1
  27. dodal/devices/diamond_filter.py +5 -17
  28. dodal/devices/eiger.py +1 -1
  29. dodal/devices/electron_analyser/__init__.py +18 -0
  30. dodal/devices/electron_analyser/abstract/__init__.py +22 -0
  31. dodal/devices/electron_analyser/abstract/base_detector.py +223 -0
  32. dodal/devices/electron_analyser/abstract/base_driver_io.py +230 -0
  33. dodal/devices/electron_analyser/{abstract_region.py → abstract/base_region.py} +3 -9
  34. dodal/devices/electron_analyser/specs/__init__.py +10 -0
  35. dodal/devices/electron_analyser/specs/detector.py +13 -0
  36. dodal/devices/electron_analyser/specs/driver_io.py +89 -0
  37. dodal/devices/electron_analyser/{specs_region.py → specs/region.py} +1 -1
  38. dodal/devices/electron_analyser/types.py +6 -0
  39. dodal/devices/electron_analyser/util.py +13 -0
  40. dodal/devices/electron_analyser/vgscienta/__init__.py +11 -0
  41. dodal/devices/electron_analyser/vgscienta/detector.py +22 -0
  42. dodal/devices/electron_analyser/vgscienta/driver_io.py +67 -0
  43. dodal/devices/electron_analyser/{vgscienta_region.py → vgscienta/region.py} +1 -2
  44. dodal/devices/fast_grid_scan.py +7 -9
  45. dodal/devices/i03/__init__.py +3 -0
  46. dodal/devices/i04/__init__.py +3 -0
  47. dodal/devices/i04/constants.py +9 -0
  48. dodal/devices/i04/murko_results.py +192 -0
  49. dodal/devices/i10/diagnostics.py +9 -61
  50. dodal/devices/i18/diode.py +37 -4
  51. dodal/devices/i24/focus_mirrors.py +9 -13
  52. dodal/devices/i24/pilatus_metadata.py +9 -9
  53. dodal/devices/i24/pmac.py +19 -14
  54. dodal/devices/{i03 → mx_phase1}/beamstop.py +26 -15
  55. dodal/devices/oav/oav_calculations.py +2 -2
  56. dodal/devices/oav/oav_detector.py +80 -32
  57. dodal/devices/oav/oav_parameters.py +46 -16
  58. dodal/devices/oav/oav_to_redis_forwarder.py +2 -2
  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/robot.py +20 -1
  63. dodal/devices/smargon.py +43 -4
  64. dodal/devices/tetramm.py +5 -2
  65. dodal/devices/util/adjuster_plans.py +1 -1
  66. dodal/devices/zebra/zebra.py +8 -0
  67. dodal/devices/zebra/zebra_constants_mapping.py +1 -1
  68. dodal/devices/zocalo/__init__.py +0 -3
  69. dodal/devices/zocalo/zocalo_results.py +6 -32
  70. dodal/log.py +14 -14
  71. dodal/plans/configure_arm_trigger_and_disarm_detector.py +167 -0
  72. dodal/common/signal_utils.py +0 -88
  73. dodal/devices/electron_analyser/abstract_analyser_io.py +0 -47
  74. dodal/devices/electron_analyser/specs_analyser_io.py +0 -19
  75. dodal/devices/electron_analyser/vgscienta_analyser_io.py +0 -26
  76. dodal/devices/logging_ophyd_device.py +0 -17
  77. dodal/plan_stubs/electron_analyser/__init__.py +0 -0
  78. dodal/plan_stubs/electron_analyser/configure_controller.py +0 -80
  79. {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/entry_points.txt +0 -0
  80. {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/licenses/LICENSE +0 -0
  81. {dls_dodal-1.46.0.dist-info → dls_dodal-1.48.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,230 @@
1
+ import asyncio
2
+ from abc import ABC, abstractmethod
3
+ from typing import Generic, TypeVar
4
+
5
+ import numpy as np
6
+ from bluesky.protocols import Movable, Preparable
7
+ from ophyd_async.core import (
8
+ Array1D,
9
+ AsyncStatus,
10
+ SignalR,
11
+ StandardReadable,
12
+ StandardReadableFormat,
13
+ derived_signal_r,
14
+ soft_signal_rw,
15
+ )
16
+ from ophyd_async.epics.adcore import ADBaseIO
17
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
18
+ from ophyd_async.epics.motor import Motor
19
+
20
+ from dodal.devices.electron_analyser.abstract.base_region import (
21
+ TAbstractBaseRegion,
22
+ )
23
+ from dodal.devices.electron_analyser.types import EnergyMode
24
+ from dodal.devices.electron_analyser.util import to_binding_energy, to_kinetic_energy
25
+
26
+
27
+ class AbstractAnalyserDriverIO(
28
+ ABC,
29
+ StandardReadable,
30
+ ADBaseIO,
31
+ Preparable,
32
+ Movable[TAbstractBaseRegion],
33
+ Generic[TAbstractBaseRegion],
34
+ ):
35
+ """
36
+ Generic device to configure electron analyser with new region settings.
37
+ Electron analysers should inherit from this class for further specialisation.
38
+ """
39
+
40
+ def __init__(self, prefix: str, name: str = "") -> None:
41
+ with self.add_children_as_readables():
42
+ self.image = epics_signal_r(Array1D[np.float64], prefix + "IMAGE")
43
+ self.spectrum = epics_signal_r(Array1D[np.float64], prefix + "INT_SPECTRUM")
44
+ self.total_intensity = derived_signal_r(
45
+ self._calculate_total_intensity, spectrum=self.spectrum
46
+ )
47
+ self.excitation_energy = soft_signal_rw(float, initial_value=0, units="eV")
48
+
49
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
50
+ # Used for setting up region data acquisition.
51
+ self.region_name = soft_signal_rw(str, initial_value="null")
52
+ self.energy_mode = soft_signal_rw(
53
+ EnergyMode, initial_value=EnergyMode.KINETIC
54
+ )
55
+ self.low_energy = epics_signal_rw(float, prefix + "LOW_ENERGY")
56
+ self.high_energy = epics_signal_rw(float, prefix + "HIGH_ENERGY")
57
+ self.slices = epics_signal_rw(int, prefix + "SLICES")
58
+ self.lens_mode = epics_signal_rw(str, prefix + "LENS_MODE")
59
+ self.pass_energy = epics_signal_rw(
60
+ self.pass_energy_type, prefix + "PASS_ENERGY"
61
+ )
62
+ self.energy_step = epics_signal_rw(float, prefix + "STEP_SIZE")
63
+ self.iterations = epics_signal_rw(int, prefix + "NumExposures")
64
+ self.acquisition_mode = epics_signal_rw(str, prefix + "ACQ_MODE")
65
+ self.excitation_energy_source = soft_signal_rw(str, initial_value="")
66
+
67
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
68
+ # Read once per scan after data acquired
69
+ self.energy_axis = self._create_energy_axis_signal(prefix)
70
+ self.binding_energy_axis = derived_signal_r(
71
+ self._calculate_binding_energy_axis,
72
+ "eV",
73
+ energy_axis=self.energy_axis,
74
+ excitation_energy=self.excitation_energy,
75
+ energy_mode=self.energy_mode,
76
+ )
77
+ self.angle_axis = self._create_angle_axis_signal(prefix)
78
+ self.step_time = epics_signal_r(float, prefix + "AcquireTime")
79
+ self.total_steps = epics_signal_r(int, prefix + "TOTAL_POINTS_RBV")
80
+ self.total_time = derived_signal_r(
81
+ self._calculate_total_time,
82
+ "s",
83
+ total_steps=self.total_steps,
84
+ step_time=self.step_time,
85
+ iterations=self.iterations,
86
+ )
87
+
88
+ super().__init__(prefix=prefix, name=name)
89
+
90
+ @AsyncStatus.wrap
91
+ async def prepare(self, value: Motor):
92
+ """
93
+ Prepare the driver for a region by passing in the energy source motor selected
94
+ by a region.
95
+
96
+ Args:
97
+ value: The motor that contains the information on the current excitation
98
+ energy. Needed to prepare region for epics to accuratly calculate
99
+ kinetic energy for an energy scan when in binding energy mode.
100
+ """
101
+ energy_source = value
102
+ excitation_energy_value = await energy_source.user_readback.get_value() # eV
103
+ excitation_energy_source_name = energy_source.name
104
+
105
+ await asyncio.gather(
106
+ self.excitation_energy.set(excitation_energy_value),
107
+ self.excitation_energy_source.set(excitation_energy_source_name),
108
+ )
109
+
110
+ @AsyncStatus.wrap
111
+ async def set(self, region: TAbstractBaseRegion):
112
+ """
113
+ This should encompass all core region logic which is common to every electron
114
+ analyser for setting up the driver.
115
+
116
+ Args:
117
+ region: Contains the parameters to setup the driver for a scan.
118
+ """
119
+ pass_energy_type = self.pass_energy_type
120
+ pass_energy = pass_energy_type(region.pass_energy)
121
+
122
+ excitation_energy = await self.excitation_energy.get_value()
123
+ low_energy = to_kinetic_energy(
124
+ region.low_energy, region.energy_mode, excitation_energy
125
+ )
126
+ high_energy = to_kinetic_energy(
127
+ region.high_energy, region.energy_mode, excitation_energy
128
+ )
129
+ await asyncio.gather(
130
+ self.region_name.set(region.name),
131
+ self.energy_mode.set(region.energy_mode),
132
+ self.low_energy.set(low_energy),
133
+ self.high_energy.set(high_energy),
134
+ self.slices.set(region.slices),
135
+ self.lens_mode.set(region.lens_mode),
136
+ self.pass_energy.set(pass_energy),
137
+ self.iterations.set(region.iterations),
138
+ self.acquisition_mode.set(region.acquisition_mode),
139
+ )
140
+
141
+ @abstractmethod
142
+ def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
143
+ """
144
+ The signal that defines the angle axis. Depends on analyser model.
145
+
146
+ Args:
147
+ prefix: PV string used for connecting to angle axis.
148
+
149
+ Returns:
150
+ Signal that can give us angle axis array data.
151
+ """
152
+
153
+ @abstractmethod
154
+ def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
155
+ """
156
+ The signal that defines the energy axis. Depends on analyser model.
157
+
158
+ Args:
159
+ prefix: PV string used for connecting to energy axis.
160
+
161
+ Returns:
162
+ Signal that can give us energy axis array data.
163
+ """
164
+
165
+ def _calculate_binding_energy_axis(
166
+ self,
167
+ energy_axis: Array1D[np.float64],
168
+ excitation_energy: float,
169
+ energy_mode: EnergyMode,
170
+ ) -> Array1D[np.float64]:
171
+ """
172
+ Calculate the binding energy axis to calibrate the spectra data. Function for a
173
+ derived signal.
174
+
175
+ Args:
176
+ energy_axis: Array data of the original energy_axis from epics.
177
+ excitation_energy: The excitation energy value used for the scan of this
178
+ region.
179
+ energy_mode: The energy_mode of the region that was used for the scan
180
+ of this region.
181
+
182
+ Returns:
183
+ Array that is the correct axis for the spectra data.
184
+ """
185
+ is_binding = energy_mode == EnergyMode.BINDING
186
+ return np.array(
187
+ [
188
+ to_binding_energy(i_energy_axis, EnergyMode.KINETIC, excitation_energy)
189
+ if is_binding
190
+ else i_energy_axis
191
+ for i_energy_axis in energy_axis
192
+ ]
193
+ )
194
+
195
+ def _calculate_total_time(
196
+ self, total_steps: int, step_time: float, iterations: int
197
+ ) -> float:
198
+ """
199
+ Calulcate the total time the scan takes for this region. Function for a derived
200
+ signal.
201
+
202
+ Args:
203
+ total_steps: Number of steps for the region.
204
+ step_time: Time for each step for the region.
205
+ iterations: The number of iterations the region collected data for.
206
+
207
+ Returns:
208
+ Calculated total time in seconds.
209
+ """
210
+ return total_steps * step_time * iterations
211
+
212
+ def _calculate_total_intensity(self, spectrum: Array1D[np.float64]) -> float:
213
+ return float(np.sum(spectrum, dtype=np.float64))
214
+
215
+ @property
216
+ @abstractmethod
217
+ def pass_energy_type(self) -> type:
218
+ """
219
+ Return the type the pass_energy should be. Depends on underlying analyser
220
+ software.
221
+
222
+ Returns:
223
+ Type the pass energy parameter from a region needs to be cast to so it can
224
+ be set correctly on the signal.
225
+ """
226
+
227
+
228
+ TAbstractAnalyserDriverIO = TypeVar(
229
+ "TAbstractAnalyserDriverIO", bound=AbstractAnalyserDriverIO
230
+ )
@@ -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
@@ -57,6 +53,7 @@ class AbstractBaseRegion(ABC, JavaToPythonModel):
57
53
  enabled: bool = False
58
54
  slices: int = 1
59
55
  iterations: int = 1
56
+ excitation_energy_source: str = "source1"
60
57
  # These ones we need subclasses to provide default values
61
58
  lens_mode: str
62
59
  pass_energy: int
@@ -73,9 +70,6 @@ class AbstractBaseRegion(ABC, JavaToPythonModel):
73
70
  def is_kinetic_energy(self) -> bool:
74
71
  return self.energy_mode == EnergyMode.KINETIC
75
72
 
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
73
  @model_validator(mode="before")
80
74
  @classmethod
81
75
  def before_validation(cls, data: dict) -> dict:
@@ -0,0 +1,10 @@
1
+ from .detector import SpecsDetector
2
+ from .driver_io import SpecsAnalyserDriverIO
3
+ from .region import SpecsRegion, SpecsSequence
4
+
5
+ __all__ = [
6
+ "SpecsDetector",
7
+ "SpecsAnalyserDriverIO",
8
+ "SpecsRegion",
9
+ "SpecsSequence",
10
+ ]
@@ -0,0 +1,13 @@
1
+ from dodal.devices.electron_analyser.abstract.base_detector import (
2
+ ElectronAnalyserDetector,
3
+ )
4
+ from dodal.devices.electron_analyser.specs.driver_io import SpecsAnalyserDriverIO
5
+ from dodal.devices.electron_analyser.specs.region import SpecsRegion, SpecsSequence
6
+
7
+
8
+ class SpecsDetector(
9
+ ElectronAnalyserDetector[SpecsAnalyserDriverIO, SpecsSequence, SpecsRegion]
10
+ ):
11
+ def __init__(self, prefix: str, name: str = ""):
12
+ driver = SpecsAnalyserDriverIO(prefix=prefix)
13
+ super().__init__(prefix, SpecsSequence, driver, name)
@@ -0,0 +1,89 @@
1
+ import asyncio
2
+
3
+ import numpy as np
4
+ from ophyd_async.core import (
5
+ Array1D,
6
+ AsyncStatus,
7
+ SignalR,
8
+ StandardReadableFormat,
9
+ derived_signal_r,
10
+ )
11
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
12
+
13
+ from dodal.devices.electron_analyser.abstract.base_driver_io import (
14
+ AbstractAnalyserDriverIO,
15
+ )
16
+ from dodal.devices.electron_analyser.specs.region import SpecsRegion
17
+
18
+
19
+ class SpecsAnalyserDriverIO(AbstractAnalyserDriverIO[SpecsRegion]):
20
+ def __init__(self, prefix: str, name: str = "") -> None:
21
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
22
+ # Used for setting up region data acquisition.
23
+ self.psu_mode = epics_signal_rw(str, prefix + "SCAN_RANGE")
24
+ self.snapshot_values = epics_signal_rw(int, prefix + "VALUES")
25
+ self.centre_energy = epics_signal_rw(float, prefix + "KINETIC_ENERGY")
26
+
27
+ # Used to read detector data after acqusition.
28
+ self.min_angle_axis = epics_signal_r(float, prefix + "Y_MIN_RBV")
29
+ self.max_angle_axis = epics_signal_r(float, prefix + "Y_MAX_RBV")
30
+
31
+ super().__init__(prefix, name)
32
+
33
+ @AsyncStatus.wrap
34
+ async def set(self, region: SpecsRegion):
35
+ await super().set(region)
36
+
37
+ await asyncio.gather(
38
+ self.snapshot_values.set(region.values),
39
+ self.psu_mode.set(region.psu_mode),
40
+ )
41
+ # ToDo - This needs to be changed to an Enum
42
+ # https://github.com/DiamondLightSource/dodal/issues/1258
43
+ if region.acquisition_mode == "Fixed Transmission":
44
+ await self.centre_energy.set(region.centre_energy)
45
+
46
+ if self.acquisition_mode == "Fixed Energy":
47
+ await self.energy_step.set(region.energy_step)
48
+
49
+ def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
50
+ angle_axis = derived_signal_r(
51
+ self._calculate_angle_axis,
52
+ min_angle=self.min_angle_axis,
53
+ max_angle=self.max_angle_axis,
54
+ slices=self.slices,
55
+ )
56
+ return angle_axis
57
+
58
+ def _calculate_angle_axis(
59
+ self, min_angle: float, max_angle: float, slices: int
60
+ ) -> Array1D[np.float64]:
61
+ # SPECS returns the extreme edges of the range, not the centre of the pixels
62
+ width = (max_angle - min_angle) / slices
63
+ offset = width / 2
64
+
65
+ axis = np.array([min_angle + offset + i * width for i in range(slices)])
66
+ return axis
67
+
68
+ def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
69
+ energy_axis = derived_signal_r(
70
+ self._calculate_energy_axis,
71
+ "eV",
72
+ min_energy=self.low_energy,
73
+ max_energy=self.high_energy,
74
+ total_points_iterations=self.slices,
75
+ )
76
+ return energy_axis
77
+
78
+ def _calculate_energy_axis(
79
+ self, min_energy: float, max_energy: float, total_points_iterations: int
80
+ ) -> Array1D[np.float64]:
81
+ # Note: Don't use the energy step because of the case where the step doesn't
82
+ # exactly fill the range
83
+ step = (max_energy - min_energy) / (total_points_iterations - 1)
84
+ axis = np.array([min_energy + i * step for i in range(total_points_iterations)])
85
+ return axis
86
+
87
+ @property
88
+ def pass_energy_type(self) -> type:
89
+ 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,11 @@
1
+ from .detector import VGScientaDetector
2
+ from .driver_io import VGScientaAnalyserDriverIO
3
+ from .region import VGScientaExcitationEnergySource, VGScientaRegion, VGScientaSequence
4
+
5
+ __all__ = [
6
+ "VGScientaDetector",
7
+ "VGScientaAnalyserDriverIO",
8
+ "VGScientaExcitationEnergySource",
9
+ "VGScientaRegion",
10
+ "VGScientaSequence",
11
+ ]
@@ -0,0 +1,22 @@
1
+ from dodal.devices.electron_analyser.abstract.base_detector import (
2
+ ElectronAnalyserDetector,
3
+ )
4
+ from dodal.devices.electron_analyser.vgscienta.driver_io import (
5
+ VGScientaAnalyserDriverIO,
6
+ )
7
+ from dodal.devices.electron_analyser.vgscienta.region import (
8
+ VGScientaRegion,
9
+ VGScientaSequence,
10
+ )
11
+
12
+
13
+ class VGScientaDetector(
14
+ ElectronAnalyserDetector[
15
+ VGScientaAnalyserDriverIO,
16
+ VGScientaSequence,
17
+ VGScientaRegion,
18
+ ]
19
+ ):
20
+ def __init__(self, prefix: str, name: str = ""):
21
+ driver = VGScientaAnalyserDriverIO(prefix)
22
+ super().__init__(prefix, VGScientaSequence, driver, name)
@@ -0,0 +1,67 @@
1
+ import asyncio
2
+
3
+ import numpy as np
4
+ from ophyd_async.core import (
5
+ Array1D,
6
+ AsyncStatus,
7
+ SignalR,
8
+ StandardReadableFormat,
9
+ )
10
+ from ophyd_async.epics.adcore import ADImageMode
11
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
12
+
13
+ from dodal.devices.electron_analyser.abstract.base_driver_io import (
14
+ AbstractAnalyserDriverIO,
15
+ )
16
+ from dodal.devices.electron_analyser.util import to_kinetic_energy
17
+ from dodal.devices.electron_analyser.vgscienta.region import (
18
+ DetectorMode,
19
+ VGScientaRegion,
20
+ )
21
+
22
+
23
+ class VGScientaAnalyserDriverIO(AbstractAnalyserDriverIO[VGScientaRegion]):
24
+ def __init__(self, prefix: str, name: str = "") -> None:
25
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
26
+ # Used for setting up region data acquisition.
27
+ self.centre_energy = epics_signal_rw(float, prefix + "CENTRE_ENERGY")
28
+ self.first_x_channel = epics_signal_rw(int, prefix + "MinX")
29
+ self.first_y_channel = epics_signal_rw(int, prefix + "MinY")
30
+ self.x_channel_size = epics_signal_rw(int, prefix + "SizeX")
31
+ self.y_channel_size = epics_signal_rw(int, prefix + "SizeY")
32
+ self.detector_mode = epics_signal_rw(DetectorMode, prefix + "DETECTOR_MODE")
33
+
34
+ with self.add_children_as_readables():
35
+ # Used to read detector data after acqusition.
36
+ self.external_io = epics_signal_r(Array1D[np.float64], prefix + "EXTIO")
37
+
38
+ super().__init__(prefix, name)
39
+
40
+ @AsyncStatus.wrap
41
+ async def set(self, region: VGScientaRegion):
42
+ await super().set(region)
43
+
44
+ excitation_energy = await self.excitation_energy.get_value()
45
+ centre_energy = to_kinetic_energy(
46
+ region.fix_energy, region.energy_mode, excitation_energy
47
+ )
48
+ await asyncio.gather(
49
+ self.centre_energy.set(centre_energy),
50
+ self.energy_step.set(region.energy_step),
51
+ self.first_x_channel.set(region.first_x_channel),
52
+ self.first_y_channel.set(region.first_y_channel),
53
+ self.x_channel_size.set(region.x_channel_size()),
54
+ self.y_channel_size.set(region.y_channel_size()),
55
+ self.detector_mode.set(region.detector_mode),
56
+ self.image_mode.set(ADImageMode.SINGLE),
57
+ )
58
+
59
+ def _create_energy_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
60
+ return epics_signal_r(Array1D[np.float64], prefix + "X_SCALE_RBV")
61
+
62
+ def _create_angle_axis_signal(self, prefix: str) -> SignalR[Array1D[np.float64]]:
63
+ return epics_signal_r(Array1D[np.float64], prefix + "Y_SCALE_RBV")
64
+
65
+ @property
66
+ def pass_energy_type(self) -> type:
67
+ 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,
@@ -40,7 +40,6 @@ class VGScientaRegion(AbstractBaseRegion):
40
40
  energy_step: float = Field(default=200.0)
41
41
  # Specific to this class
42
42
  id: str = Field(default=str(uuid.uuid4()), alias="region_id")
43
- excitation_energy_source: str = "source1"
44
43
  fix_energy: float = 9.0
45
44
  total_steps: float = 13.0
46
45
  total_time: float = 13.0
@@ -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: int, y: int, z: int) -> int:
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"]
@@ -0,0 +1,3 @@
1
+ from dodal.devices.mx_phase1.beamstop import Beamstop, BeamstopPositions
2
+
3
+ __all__ = ["Beamstop", "BeamstopPositions"]
@@ -0,0 +1,9 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class RedisConstants:
7
+ REDIS_HOST = os.environ.get("VALKEY_PROD_SVC_SERVICE_HOST", "test_redis")
8
+ REDIS_PASSWORD = os.environ.get("VALKEY_PASSWORD", "test_redis_password")
9
+ MURKO_REDIS_DB = 7