dls-dodal 1.36.2__py3-none-any.whl → 1.37.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 (54) hide show
  1. {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/METADATA +4 -4
  2. {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/RECORD +54 -38
  3. {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/WHEEL +1 -1
  4. dodal/_version.py +2 -2
  5. dodal/beamlines/i02_1.py +37 -0
  6. dodal/beamlines/i03.py +20 -3
  7. dodal/beamlines/i04.py +3 -3
  8. dodal/beamlines/i10.py +179 -7
  9. dodal/beamlines/i22.py +15 -0
  10. dodal/beamlines/i24.py +2 -2
  11. dodal/beamlines/p99.py +6 -2
  12. dodal/common/crystal_metadata.py +3 -3
  13. dodal/common/udc_directory_provider.py +3 -1
  14. dodal/devices/aperturescatterguard.py +3 -0
  15. dodal/devices/apple2_undulator.py +9 -9
  16. dodal/devices/{attenuator.py → attenuator/attenuator.py} +29 -1
  17. dodal/devices/attenuator/filter.py +11 -0
  18. dodal/devices/attenuator/filter_selections.py +72 -0
  19. dodal/devices/bimorph_mirror.py +151 -0
  20. dodal/devices/current_amplifiers/__init__.py +34 -0
  21. dodal/devices/current_amplifiers/current_amplifier.py +103 -0
  22. dodal/devices/current_amplifiers/current_amplifier_detector.py +109 -0
  23. dodal/devices/current_amplifiers/femto.py +143 -0
  24. dodal/devices/current_amplifiers/sr570.py +214 -0
  25. dodal/devices/current_amplifiers/struck_scaler_counter.py +79 -0
  26. dodal/devices/detector/det_dim_constants.py +15 -0
  27. dodal/devices/eiger_odin.py +3 -3
  28. dodal/devices/fast_grid_scan.py +8 -3
  29. dodal/devices/i03/beamstop.py +85 -0
  30. dodal/devices/i04/transfocator.py +67 -53
  31. dodal/devices/i10/i10_setting_data.py +3 -3
  32. dodal/devices/i10/mirrors.py +24 -0
  33. dodal/devices/i10/rasor/rasor_current_amp.py +72 -0
  34. dodal/devices/i10/rasor/rasor_motors.py +62 -0
  35. dodal/devices/i10/rasor/rasor_scaler_cards.py +12 -0
  36. dodal/devices/i10/slits.py +37 -0
  37. dodal/devices/i24/dual_backlight.py +1 -0
  38. dodal/devices/i24/focus_mirrors.py +12 -12
  39. dodal/devices/linkam3.py +2 -2
  40. dodal/devices/p99/sample_stage.py +2 -28
  41. dodal/devices/robot.py +2 -2
  42. dodal/devices/slits.py +29 -7
  43. dodal/devices/tetramm.py +16 -16
  44. dodal/devices/undulator_dcm.py +9 -11
  45. dodal/devices/util/test_utils.py +2 -2
  46. dodal/devices/xspress3/xspress3.py +3 -3
  47. dodal/devices/zebra.py +19 -14
  48. dodal/devices/zocalo/zocalo_interaction.py +2 -1
  49. dodal/devices/zocalo/zocalo_results.py +22 -2
  50. dodal/log.py +2 -2
  51. dodal/plans/wrapped.py +3 -3
  52. {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/LICENSE +0 -0
  53. {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/entry_points.txt +0 -0
  54. {dls_dodal-1.36.2.dist-info → dls_dodal-1.37.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,143 @@
1
+ import asyncio
2
+ from enum import Enum
3
+
4
+ from ophyd_async.core import (
5
+ AsyncStatus,
6
+ StandardReadableFormat,
7
+ StrictEnum,
8
+ )
9
+ from ophyd_async.epics.core import epics_signal_rw
10
+
11
+ from dodal.devices.current_amplifiers import CurrentAmp
12
+ from dodal.log import LOGGER
13
+
14
+
15
+ class Femto3xxGainTable(StrictEnum):
16
+ """These are the sensitivity setting for Femto 3xx current amplifier"""
17
+
18
+ SEN_1 = "10^4"
19
+ SEN_2 = "10^5"
20
+ SEN_3 = "10^6"
21
+ SEN_4 = "10^7"
22
+ SEN_5 = "10^8"
23
+ SEN_6 = "10^9"
24
+ SEN_7 = "10^10"
25
+ SEN_8 = "10^11"
26
+ SEN_9 = "10^12"
27
+ SEN_10 = "10^13"
28
+
29
+
30
+ class Femto3xxGainToCurrentTable(float, Enum):
31
+ """These are the voltage to current setting for Femto 3xx current amplifier"""
32
+
33
+ SEN_1 = 1e4
34
+ SEN_2 = 1e5
35
+ SEN_3 = 1e6
36
+ SEN_4 = 1e7
37
+ SEN_5 = 1e8
38
+ SEN_6 = 1e9
39
+ SEN_7 = 1e10
40
+ SEN_8 = 1e11
41
+ SEN_9 = 1e12
42
+ SEN_10 = 1e13
43
+
44
+
45
+ class Femto3xxRaiseTime(float, Enum):
46
+ """These are the gain dependent raise time(s) for Femto 3xx current amplifier"""
47
+
48
+ SEN_1 = 0.8e-3
49
+ SEN_2 = 0.8e-3
50
+ SEN_3 = 0.8e-3
51
+ SEN_4 = 0.8e-3
52
+ SEN_5 = 2.3e-3
53
+ SEN_6 = 2.3e-3
54
+ SEN_7 = 17e-3
55
+ SEN_8 = 17e-3
56
+ SEN_9 = 350e-3
57
+ SEN_10 = 350e-3
58
+
59
+
60
+ class FemtoDDPCA(CurrentAmp):
61
+ """
62
+ Femto current amplifier device, this class should cover all DDPCA Femto current
63
+ amplifiers, As the main different between all the DDPCA Femto is their gain table
64
+ and response time table.
65
+ Attributes:
66
+ gain (SignalRW): This is the epic signal that control current amplifier gain.
67
+ gain_table (strictEnum): The table epic use to set gain.
68
+ upperlimit (float): upperlimit of the current amplifier
69
+ lowerlimit (float): lowerlimit of the current amplifier
70
+ timeout (float): Maximum waiting time in second for setting gain.
71
+ raise_timetable (Enum): Table contain the minimum amount of time to wait after
72
+ changing gain.
73
+
74
+ """
75
+
76
+ def __init__(
77
+ self,
78
+ prefix: str,
79
+ suffix: str,
80
+ gain_table: type[StrictEnum],
81
+ gain_to_current_table: type[Enum],
82
+ raise_timetable: type[Enum],
83
+ upperlimit: float = 8.8,
84
+ lowerlimit: float = 0.68,
85
+ timeout: float = 1,
86
+ name: str = "",
87
+ ) -> None:
88
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
89
+ self.gain = epics_signal_rw(gain_table, prefix + suffix)
90
+
91
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
92
+ self.gain_table = gain_table
93
+ self.timeout = timeout
94
+ self.raise_timetable = raise_timetable
95
+ self.upperlimit = upperlimit
96
+ self.lowerlimit = lowerlimit
97
+ super().__init__(name=name, gain_conversion_table=gain_to_current_table)
98
+
99
+ @AsyncStatus.wrap
100
+ async def set(self, value) -> None:
101
+ if value not in [item.value for item in self.gain_conversion_table]:
102
+ raise ValueError(
103
+ f"Gain value {value} is not within {self.name} range."
104
+ + "\n Available gain:"
105
+ + f" {[f'{c.value:.0e}' for c in self.gain_conversion_table]}"
106
+ )
107
+ SEN_setting = self.gain_conversion_table(value).name
108
+ LOGGER.info(f"{self.name} gain change to {SEN_setting}:{value}")
109
+
110
+ await self.gain.set(
111
+ value=self.gain_table[SEN_setting].value,
112
+ timeout=self.timeout,
113
+ )
114
+ # wait for current amplifier's bandpass filter to settle.
115
+ await asyncio.sleep(self.raise_timetable[SEN_setting].value)
116
+
117
+ @AsyncStatus.wrap
118
+ async def increase_gain(self, value: int = 1) -> None:
119
+ current_gain = int((await self.get_gain()).name.split("_")[-1])
120
+ current_gain += value
121
+ if current_gain > len(self.gain_table):
122
+ raise ValueError("Gain at max value")
123
+ await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
124
+
125
+ @AsyncStatus.wrap
126
+ async def decrease_gain(self, value: int = 1) -> None:
127
+ current_gain = int((await self.get_gain()).name.split("_")[-1])
128
+ current_gain -= value
129
+ if current_gain < 1:
130
+ raise ValueError("Gain at min value")
131
+ await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
132
+
133
+ @AsyncStatus.wrap
134
+ async def get_gain(self) -> Enum:
135
+ return self.gain_conversion_table[(await self.gain.get_value()).name]
136
+
137
+ @AsyncStatus.wrap
138
+ async def get_upperlimit(self) -> float:
139
+ return self.upperlimit
140
+
141
+ @AsyncStatus.wrap
142
+ async def get_lowerlimit(self) -> float:
143
+ return self.lowerlimit
@@ -0,0 +1,214 @@
1
+ import asyncio
2
+ from enum import Enum
3
+
4
+ from ophyd_async.core import (
5
+ AsyncStatus,
6
+ StandardReadableFormat,
7
+ StrictEnum,
8
+ )
9
+ from ophyd_async.epics.core import epics_signal_rw
10
+
11
+ from dodal.devices.current_amplifiers.current_amplifier import CurrentAmp
12
+ from dodal.log import LOGGER
13
+
14
+
15
+ class SR570GainTable(StrictEnum):
16
+ """Coarse/unit sensitivity setting for SR570 current amplifier"""
17
+
18
+ SEN_1 = "mA/V"
19
+ SEN_2 = "uA/V"
20
+ SEN_3 = "nA/V"
21
+ SEN_4 = "pA/V"
22
+
23
+
24
+ class SR570FineGainTable(StrictEnum):
25
+ """Fine sensitivity setting for SR570 current amplifier"""
26
+
27
+ SEN_1 = "1"
28
+ SEN_2 = "2"
29
+ SEN_3 = "5"
30
+ SEN_4 = "10"
31
+ SEN_5 = "20"
32
+ SEN_6 = "50"
33
+ SEN_7 = "100"
34
+ SEN_8 = "200"
35
+ SEN_9 = "500"
36
+
37
+
38
+ class SR570RaiseTimeTable(float, Enum):
39
+ """These are the gain dependent raise time(s) for SR570 current amplifier"""
40
+
41
+ SEN_1 = 1e-4
42
+ SEN_2 = 1e-2
43
+ SEN_3 = 0.15
44
+ SEN_4 = 0.2
45
+
46
+
47
+ class SR570FullGainTable(Enum):
48
+ """Combined gain table, as each gain step is a combination of both coarse gain and
49
+ fine gain setting"""
50
+
51
+ SEN_1 = [SR570GainTable.SEN_1, SR570FineGainTable.SEN_1]
52
+ SEN_2 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_9]
53
+ SEN_3 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_8]
54
+ SEN_4 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_7]
55
+ SEN_5 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_6]
56
+ SEN_6 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_5]
57
+ SEN_7 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_4]
58
+ SEN_8 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_3]
59
+ SEN_9 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_2]
60
+ SEN_10 = [SR570GainTable.SEN_2, SR570FineGainTable.SEN_1]
61
+ SEN_11 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_9]
62
+ SEN_12 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_8]
63
+ SEN_13 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_7]
64
+ SEN_14 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_6]
65
+ SEN_15 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_5]
66
+ SEN_16 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_4]
67
+ SEN_17 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_3]
68
+ SEN_18 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_2]
69
+ SEN_19 = [SR570GainTable.SEN_3, SR570FineGainTable.SEN_1]
70
+ SEN_20 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_9]
71
+ SEN_21 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_8]
72
+ SEN_22 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_7]
73
+ SEN_23 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_6]
74
+ SEN_24 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_5]
75
+ SEN_25 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_4]
76
+ SEN_26 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_3]
77
+ SEN_27 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_2]
78
+ SEN_28 = [SR570GainTable.SEN_4, SR570FineGainTable.SEN_1]
79
+
80
+
81
+ class SR570GainToCurrentTable(float, Enum):
82
+ """Conversion table for gain(sen) to current"""
83
+
84
+ SEN_1 = 1e3
85
+ SEN_2 = 2e3
86
+ SEN_3 = 5e3
87
+ SEN_4 = 1e4
88
+ SEN_5 = 2e4
89
+ SEN_6 = 5e4
90
+ SEN_7 = 1e5
91
+ SEN_8 = 2e5
92
+ SEN_9 = 5e5
93
+ SEN_10 = 1e6
94
+ SEN_11 = 2e6
95
+ SEN_12 = 5e6
96
+ SEN_13 = 1e7
97
+ SEN_14 = 2e7
98
+ SEN_15 = 5e7
99
+ SEN_16 = 1e8
100
+ SEN_17 = 2e8
101
+ SEN_18 = 5e8
102
+ SEN_19 = 1e9
103
+ SEN_20 = 2e9
104
+ SEN_21 = 5e9
105
+ SEN_22 = 1e10
106
+ SEN_23 = 2e10
107
+ SEN_24 = 5e10
108
+ SEN_25 = 1e11
109
+ SEN_26 = 2e11
110
+ SEN_27 = 5e11
111
+ SEN_28 = 1e12
112
+
113
+
114
+ class SR570(CurrentAmp):
115
+ """
116
+ SR570 current amplifier device. This is similar to Femto with the only different
117
+ is SR570 has two gain setting fine and coarse, therefore it requires extra
118
+ gain tables.
119
+ Attributes:
120
+ fine_gain (SignalRW): This is the epic signal that control SR570 fine gain.
121
+ coarse_gain (SignalRW): This is the epic signal that control SR570 coarse gain.
122
+ fine_gain_table (strictEnum): The table that fine_gain use to set gain.
123
+ coarse_gain_table (strictEnum): The table that coarse_gain use to set gain.
124
+ timeout (float): Maximum waiting time in second for setting gain.
125
+ raise_timetable (Enum): Table contain the amount of time to wait after
126
+ setting gain.
127
+ combined_table (Enum): Table that combine fine and coarse table into one.
128
+ gain (SignalRW([str]): Soft signal to store the member name of the current gain
129
+ setting in gain_conversion_table.
130
+ upperlimit (float): upperlimit of the current amplifier
131
+ lowerlimit (float): lowerlimit of the current amplifier
132
+
133
+ """
134
+
135
+ def __init__(
136
+ self,
137
+ prefix: str,
138
+ suffix: str,
139
+ fine_gain_table: type[StrictEnum],
140
+ coarse_gain_table: type[StrictEnum],
141
+ combined_table: type[Enum],
142
+ gain_to_current_table: type[Enum],
143
+ raise_timetable: type[Enum],
144
+ upperlimit: float = 4.8,
145
+ lowerlimit: float = 0.4,
146
+ timeout: float = 1,
147
+ name: str = "",
148
+ ) -> None:
149
+ super().__init__(name=name, gain_conversion_table=gain_to_current_table)
150
+
151
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
152
+ self.fine_gain = epics_signal_rw(fine_gain_table, prefix + suffix + "1")
153
+ self.coarse_gain = epics_signal_rw(coarse_gain_table, prefix + suffix + "2")
154
+
155
+ self.fine_gain_table = fine_gain_table
156
+ self.coarse_gain_table = coarse_gain_table
157
+ self.timeout = timeout
158
+ self.raise_timetable = raise_timetable
159
+ self.combined_table = combined_table
160
+ self.upperlimit = upperlimit
161
+ self.lowerlimit = lowerlimit
162
+
163
+ @AsyncStatus.wrap
164
+ async def set(self, value) -> None:
165
+ if value not in [item.value for item in self.gain_conversion_table]:
166
+ raise ValueError(
167
+ f"Gain value {value} is not within {self.name} range."
168
+ + "\n Available gain:"
169
+ + f" {[f'{c.value:.0e}' for c in self.gain_conversion_table]}"
170
+ )
171
+ SEN_setting = self.gain_conversion_table(value).name
172
+ LOGGER.info(f"{self.name} gain change to {value}")
173
+
174
+ coarse_gain, fine_gain = self.combined_table[SEN_setting].value
175
+ await asyncio.gather(
176
+ self.fine_gain.set(value=fine_gain, timeout=self.timeout),
177
+ self.coarse_gain.set(value=coarse_gain, timeout=self.timeout),
178
+ )
179
+ await asyncio.sleep(self.raise_timetable[coarse_gain.name].value)
180
+
181
+ @AsyncStatus.wrap
182
+ async def increase_gain(self, value=3) -> None:
183
+ current_gain = int((await self.get_gain()).name.split("_")[-1])
184
+ current_gain += value
185
+ if current_gain > len(self.combined_table):
186
+ await self.set(
187
+ self.gain_conversion_table[f"SEN_{len(self.combined_table)}"]
188
+ )
189
+ raise ValueError("Gain at max value")
190
+ await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
191
+
192
+ @AsyncStatus.wrap
193
+ async def decrease_gain(self, value=3) -> None:
194
+ current_gain = int((await self.get_gain()).name.split("_")[-1])
195
+ current_gain -= value
196
+ if current_gain < 1:
197
+ await self.set(self.gain_conversion_table["SEN_1"])
198
+ raise ValueError("Gain at min value")
199
+ await self.set(self.gain_conversion_table[f"SEN_{current_gain}"])
200
+
201
+ @AsyncStatus.wrap
202
+ async def get_gain(self) -> Enum:
203
+ result = await asyncio.gather(
204
+ self.coarse_gain.get_value(), self.fine_gain.get_value()
205
+ )
206
+ return self.gain_conversion_table[self.combined_table(result).name]
207
+
208
+ @AsyncStatus.wrap
209
+ async def get_upperlimit(self) -> float:
210
+ return self.upperlimit
211
+
212
+ @AsyncStatus.wrap
213
+ async def get_lowerlimit(self) -> float:
214
+ return self.lowerlimit
@@ -0,0 +1,79 @@
1
+ from ophyd_async.core import (
2
+ AsyncStatus,
3
+ StandardReadableFormat,
4
+ StrictEnum,
5
+ set_and_wait_for_other_value,
6
+ )
7
+ from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
8
+
9
+ from dodal.devices.current_amplifiers import (
10
+ CurrentAmpCounter,
11
+ )
12
+
13
+
14
+ class CountMode(StrictEnum):
15
+ AUTO = "AutoCount"
16
+ ONE_SHOT = "OneShot"
17
+
18
+
19
+ class CountState(StrictEnum):
20
+ DONE = "Done"
21
+ COUNT = "Count" # type: ignore
22
+
23
+
24
+ COUNT_PER_VOLTAGE = 100000
25
+
26
+
27
+ class StruckScaler(CurrentAmpCounter):
28
+ """
29
+ StruckScaler is a counting card that record the output signal from a wide
30
+ range of detectors. This class contains the basic control to run the struckscaler
31
+ card together with a current amplifier. It has functions that provide conversion
32
+ between count and voltage.
33
+ Attributes:
34
+ readout(SignalR): Scaler card output.
35
+ count_mode (SignalR[CountMode]): Counting card setting.
36
+ count_time (SignalRW([float]): Count time.
37
+ """
38
+
39
+ def __init__(
40
+ self, prefix: str, suffix: str, count_per_volt=COUNT_PER_VOLTAGE, name: str = ""
41
+ ):
42
+ with self.add_children_as_readables(StandardReadableFormat.HINTED_SIGNAL):
43
+ self.readout = epics_signal_r(float, prefix + suffix)
44
+ with self.add_children_as_readables(StandardReadableFormat.CONFIG_SIGNAL):
45
+ self.count_mode = epics_signal_rw(CountMode, prefix + ":AutoCount")
46
+ self.count_time = epics_signal_rw(float, prefix + ".TP")
47
+ self.trigger_start = epics_signal_rw(CountState, prefix + ".CNT")
48
+ super().__init__(count_per_volt=count_per_volt, name=name)
49
+
50
+ @AsyncStatus.wrap
51
+ async def stage(self) -> None:
52
+ await self.count_mode.set(CountMode.ONE_SHOT)
53
+
54
+ @AsyncStatus.wrap
55
+ async def unstage(self) -> None:
56
+ await self.count_mode.set(CountMode.AUTO)
57
+
58
+ @AsyncStatus.wrap
59
+ async def trigger(self) -> None:
60
+ await set_and_wait_for_other_value(
61
+ set_signal=self.trigger_start,
62
+ set_value=CountState.COUNT,
63
+ match_signal=self.trigger_start,
64
+ match_value=CountState.COUNT,
65
+ )
66
+
67
+ @AsyncStatus.wrap
68
+ async def prepare(self, value) -> None:
69
+ await self.count_time.set(value)
70
+
71
+ async def get_count(self) -> float:
72
+ await self.trigger()
73
+ return await self.readout.get_value()
74
+
75
+ async def get_count_per_sec(self) -> float:
76
+ return await self.get_count() / await self.count_time.get_value()
77
+
78
+ async def get_voltage_per_sec(self) -> float:
79
+ return await self.get_count_per_sec() / self.count_per_volt
@@ -26,6 +26,21 @@ class DetectorSizeConstants:
26
26
  ALL_DETECTORS[self.det_type_string] = self
27
27
 
28
28
 
29
+ PILATUS_TYPE_6M = "PILATUS_6M"
30
+ PILATUS_6M_DIMENSION_X = 423.636
31
+ PILATUS_6M_DIMENSION_Y = 434.644
32
+ PILATUS_6M_DIMENSION = DetectorSize(PILATUS_6M_DIMENSION_X, PILATUS_6M_DIMENSION_Y)
33
+ PIXELS_X_PILATUS_6M = 2463
34
+ PIXELS_Y_PILATUS_6M = 2527
35
+ PIXELS_PILATUS_6M = DetectorSize(PIXELS_X_PILATUS_6M, PIXELS_Y_PILATUS_6M)
36
+ PILATUS_6M_SIZE = DetectorSizeConstants(
37
+ PILATUS_TYPE_6M,
38
+ PILATUS_6M_DIMENSION,
39
+ PIXELS_PILATUS_6M,
40
+ PILATUS_6M_DIMENSION,
41
+ PIXELS_PILATUS_6M,
42
+ )
43
+
29
44
  EIGER_TYPE_EIGER2_X_4M = "EIGER2_X_4M"
30
45
  EIGER2_X_4M_DIMENSION_X = 155.1
31
46
  EIGER2_X_4M_DIMENSION_Y = 162.15
@@ -91,10 +91,10 @@ class OdinNodesStatus(Device):
91
91
  def wait_for_no_errors(self, timeout) -> dict[SubscriptionStatus, str]:
92
92
  errors = {}
93
93
  for node_number, node_pv in enumerate(self.nodes):
94
- errors[
95
- await_value(node_pv.error_status, False, timeout)
96
- ] = f"Filewriter {node_number} is in an error state with error message\
94
+ errors[await_value(node_pv.error_status, False, timeout)] = (
95
+ f"Filewriter {node_number} is in an error state with error message\
97
96
  - {node_pv.error_message.get()}"
97
+ )
98
98
 
99
99
  return errors
100
100
 
@@ -97,9 +97,14 @@ class GridScanParamsCommon(AbstractExperimentWithBeamParams):
97
97
  """Converts a grid position, given as steps in the x, y, z grid,
98
98
  to a real motor position.
99
99
 
100
- :param grid_position: The x, y, z position in grid steps
101
- :return: The motor position this corresponds to.
102
- :raises: IndexError if the desired position is outside the grid."""
100
+ Args:
101
+ grid_position: The x, y, z position in grid steps. The origin is at the
102
+ centre of the first grid box
103
+ Returns:
104
+ The motor position this corresponds to.
105
+ Raises:
106
+ IndexError if the desired position is outside the grid.
107
+ """
103
108
  for position, axis in zip(
104
109
  grid_position, [self.x_axis, self.y_axis, self.z_axis], strict=False
105
110
  ):
@@ -0,0 +1,85 @@
1
+ from asyncio import gather
2
+ from math import isclose
3
+
4
+ from ophyd_async.core import StandardReadable, StrictEnum
5
+ from ophyd_async.epics.motor import Motor
6
+
7
+ from dodal.common.beamlines.beamline_parameters import GDABeamlineParameters
8
+ from dodal.common.signal_utils import create_hardware_backed_soft_signal
9
+
10
+
11
+ class BeamstopPositions(StrictEnum):
12
+ """
13
+ Beamstop positions.
14
+ GDA supports Standard/High/Low resolution positions, as well as parked and
15
+ robot load however all 3 resolution positions are the same. We also
16
+ do not use the robot load position in Hyperion.
17
+
18
+ Until we support moving the beamstop it is only necessary to check whether the
19
+ beamstop is in beam or not.
20
+
21
+ See Also:
22
+ https://github.com/DiamondLightSource/mx-bluesky/issues/484
23
+
24
+ Attributes:
25
+ DATA_COLLECTION: The beamstop is in beam ready for data collection
26
+ UNKNOWN: The beamstop is in some other position, check the device motor
27
+ positions to determine it.
28
+ """
29
+
30
+ DATA_COLLECTION = "Data Collection"
31
+ UNKNOWN = "Unknown"
32
+
33
+
34
+ class Beamstop(StandardReadable):
35
+ """
36
+ Beamstop for I03.
37
+
38
+ Attributes:
39
+ x: beamstop x position in mm
40
+ y: beamstop y position in mm
41
+ z: beamstop z position in mm
42
+ selected_pos: Get the current position of the beamstop as an enum. Currently this
43
+ is read-only.
44
+ """
45
+
46
+ def __init__(
47
+ self,
48
+ prefix: str,
49
+ beamline_parameters: GDABeamlineParameters,
50
+ name: str = "",
51
+ ):
52
+ with self.add_children_as_readables():
53
+ self.x_mm = Motor(prefix + "X")
54
+ self.y_mm = Motor(prefix + "Y")
55
+ self.z_mm = Motor(prefix + "Z")
56
+ self.selected_pos = create_hardware_backed_soft_signal(
57
+ BeamstopPositions, self._get_selected_position
58
+ )
59
+
60
+ self._in_beam_xyz_mm = [
61
+ float(beamline_parameters[f"in_beam_{axis}_STANDARD"])
62
+ for axis in ("x", "y", "z")
63
+ ]
64
+ self._xyz_tolerance_mm = [
65
+ float(beamline_parameters[f"bs_{axis}_tolerance"])
66
+ for axis in ("x", "y", "z")
67
+ ]
68
+
69
+ super().__init__(name)
70
+
71
+ async def _get_selected_position(self) -> BeamstopPositions:
72
+ current_pos = await gather(
73
+ self.x_mm.user_readback.get_value(),
74
+ self.y_mm.user_readback.get_value(),
75
+ self.z_mm.user_readback.get_value(),
76
+ )
77
+ if all(
78
+ isclose(axis_pos, axis_in_beam, abs_tol=axis_tolerance)
79
+ for axis_pos, axis_in_beam, axis_tolerance in zip(
80
+ current_pos, self._in_beam_xyz_mm, self._xyz_tolerance_mm, strict=False
81
+ )
82
+ ):
83
+ return BeamstopPositions.DATA_COLLECTION
84
+ else:
85
+ return BeamstopPositions.UNKNOWN